个人为了秋招整理一些知识点,理清思路,大佬们发现不对可以指正。
图片和一部分文字来自:
https://ty-chen.github.io/linux-kernel-handshake/#more
- bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
struct in_addr {
__be32 s_addr;
};
bind()的逻辑如下:
调用sockfd_lookup_light(),根据文件描述符找到socket结构,通过move_addr_to_kernel()将sockaddr拷贝到内核,在调用socket的ops结构中inet_bind(),在这个函数内部首先检查端口是否冲突,能否用来绑定,通过则设置本地地址和端口,将对方地址初始化为0;
2.listen()
当我们调用listen(fd, backlog)时,其实是调用sock->ops->listen(),其流程顺序为先获取fd指向的socket对象(不知道是通过file->private_data,还是维持着额外的数据结构加快查找),全连接队列长度是listen传入backlog和内核somaxconn中的较小值,后面传入inet_listen(),判断sock是否为监听状态,不是就调用inet_csk_listen_start,内部将socket对象内的inet_sock强转成inet_connection_sock对象(其中sock.inet_sock.inet_connection_sock.tcp_sock是逐层嵌套的关系),如果打开它,你能看到处于各种状态的队列,各种超时时间、拥塞控制等字眼。我们说 TCP 是面向连接的,就是客户端和服务端都是有一个结构维护连接的状态,就是指这个结构;
随后将inet_connection_sock->icsk_accept_queue进行初始化,其内部包含一个全连接队列,以及一个半连接队列(一个listen_sock对象,选择用一个hash表管理,便于查找第一次握手对象request_sock)在 listen 的过程中,内核我们也看到了对于半连接队列来说,其最大长度是 min(backlog, somaxconn, tcp_max_syn_backlog) + 1 再上取整到 2 的幂次,但最小不能小于16。
struct request_sock_queue {
//全连接队列
struct request_sock *rskq_accept_head;
struct request_sock *rskq_accept_tail;
//半连接队列
struct listen_sock *listen_opt;
......
};
struct listen_sock {
u8 max_qlen_log;
u32 nr_table_entries;
......
struct request_sock *syn_table[0];
};
3.accept()
内部主要逻辑为:调用sockfd_lookup_light()找到对应socket对象,创建newsock,再创建newfile跟newsock绑定,调用套接字对应的accept()函数,即inet_accept()完成实际服务端握手过程
调用fd_install()关联套接字文件和套接字描述符,并返回连接的套接字描述符,inet_accept()会提取监听套接字的网络层结构体sk1和新建套接字的sk2,调用sk1协议对应的accept()完成握手并保存连接状态于sk2中,这里实际调用的是inet_csk_accept()函数。接着将sk2和新建套接字进行关联。inet_csk_accept()函数会判断当前的半连接队列rskq_accept_queue是否为空,如果空则调用inet_csk_wait_for_connect()及逆行等待。如果不为空则从队列中取出一个连接,赋值给newsk并返回。inet_csk_wait_for_connect()调用 schedule_timeout()让出 CPU,并且将进程状态设置为 TASK_INTERRUPTIBLE。如果再次 CPU 醒来,我们会接着判断 icsk_accept_queue 是否为空,同时也会调用 signal_pending 看有没有信号可以处理。一旦 icsk_accept_queue 不为空,就从 inet_csk_wait_for_connect() 中返回,在队列中取出一个 struct sock 对象赋值给 newsk。
4.connect()
connect()函数通常由客户端发起,是三次握手的开始,服务端收到了SYN之后回复ACK + SYN并将该连接加入半连接队列,进入SYN_RCVD状态,第三次握手收到ACK后从半连接队列取出,加入全连接队列,此时的 socket 处于 ESTABLISHED 状态。accept()函数唤醒后检索队列,发现有连接则继续工作下去,从队列中取出该套接字并返回,供以后续读写使用。connect()对应的系统调用如下所示,其主要逻辑为:
调用sockfd_lookup_light()查找套接字描述符fd对应的套接字sock
调用move_addr_to_kernel()将目的地址发到内核中供使用
调用初始化connect()函数或者设置的特定connect()函数,这里会调用inet_stream_connect()发起连接,判断当前套接字状态,如果尚未连接则调用 struct sock 的 sk->sk_prot->connect(),也即 tcp_prot 的 connect() 函数tcp_v4_connect() 函数发起握手。
调用inet_wait_for_connect(),等待来自于服务端的ACK信号,调用ip_route_connect()选择一条路由,根据选定的网卡填写该网卡的 IP 地址作为源IP地址
将客户端状态设置为TCP_SYN_SENT,初始化序列号write_seq
调用tcp_connect()发送SYN包,创建新的结构体 struct tcp_sock该结构体是 struct inet_connection_sock 的一个扩展,维护了更多的 TCP 的状态
调用tcp_init_nondata_skb() 初始化一个 SYN 包
调用tcp_transmit_skb() 将 SYN 包发送出去
调用inet_csk_reset_xmit_timer() 设置了一个 timer,如果 SYN 发送不成功,则再次发送。关于底层发包收包留到后面单独解析,这里先重点看三次握手的过程。当发包完成后,我们会等待接收ACK,接收数据包的调用链为tcp_v4_rcv()->tcp_v4_do_rcv()->tcp_rcv_state_process()。tcp_rcv_state_process()是一个服务端客户端通用函数,根据状态位来判断如何执行。
当服务端处于TCP_LISTEN状态时,收到第一次握手即客户端的SYN,调用conn_request()进行处理,其实调用的是 tcp_v4_conn_request(),更新自身状态、队列,然后调用tcp_v4_send_synack()发送第二次握手消息,进入状态TCP_SYN_RECV
当客户端处于TCP_SYN_SENT状态时,收到服务端返回的第二次握手消息ACK + SYN,调用tcp_rcv_synsent_state_process()进行处理,调用tcp_send_ack()发送ACK回复给服务端,进入TCP_ESTABLISHED状态
服务端处于TCP_SYN_RECV状态时,收到客户端返回的第三次握手消息ACK,进入TCP_ESTABLISHED状态