主要来了解一下Socket在系统内核的实现。
socket函数 socket系统调用的主要代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SYSCALL_DEFINE3(socket, int , family, int , type, int , protocol) { int retval; struct socket *sock ; int flags; ...... if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; retval = sock_create(family, type, protocol, &sock); ...... retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); ...... return retval; }
调用 sock_create 创建一个 struct socket 结构,然后通过 sock_map_fd 和文件描述符对应起来。
参数family表示地址族,主要是2种,单机的基于文件的AF_UNIX, 支持多机的基于IP网络的AF_INET。还有其他类型的,可以用到在去看(比如Ipv6的AF_INET6)。
参数type表示socket的类型, 也只有几种:分别是 SOCK_STREAM、SOCK_DGRAM 和 SOCK_RAW。。
参数protocol 表示协议的。协议数目是比较多的,也就是说,多个协议会属于同一种类型。
SOCK_STREAM 是面向数据流的,协议 IPPROTO_TCP 属于这种类型。SOCK_DGRAM 是面向数据报的,协议 IPPROTO_UDP 属于这种类型。如果在内核里面看的话,IPPROTO_ICMP 也属于这种类型。SOCK_RAW 是原始的 IP 包,IPPROTO_IP 属于这种类型。
内核中有专门的数据结构来保存上面几个参数代表的数据。
1 2 3 4 5 6 7 static const struct net_proto_family inet_family_ops = { .family = PF_INET, .create = inet_create, ...... }
地址族有数据结构struct net_proto_family 保存,inet_create用来创建socket。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 static struct inet_protosw inetsw_array [] ={ { .type = SOCK_STREAM, .protocol = IPPROTO_TCP, .prot = &tcp_prot, .ops = &inet_stream_ops, .flags = INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK, }, { .type = SOCK_DGRAM, .protocol = IPPROTO_UDP, .prot = &udp_prot, .ops = &inet_dgram_ops, .flags = INET_PROTOSW_PERMANENT, }, { .type = SOCK_DGRAM, .protocol = IPPROTO_ICMP, .prot = &ping_prot, .ops = &inet_sockraw_ops, .flags = INET_PROTOSW_REUSE, }, { .type = SOCK_RAW, .protocol = IPPROTO_IP, .prot = &raw_prot, .ops = &inet_sockraw_ops, .flags = INET_PROTOSW_REUSE, } }
type和protocl由struct inet_protosw来保存。这里面保存对应类型的一些属性和操作函数。然后,在具体调用的时候获取到对应类型对应的结构,而后再调用。这个和以前讲的文件的操作的实现是一样样的。具体的就不深入了。
bind函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 SYSCALL_DEFINE3(bind, int , fd, struct sockaddr __user *, umyaddr, int , addrlen) { struct socket *sock ; struct sockaddr_storage address ; int err, fput_needed; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { err = move_addr_to_kernel(umyaddr, addrlen, &address); if (err >= 0 ) { err = sock->ops->bind(sock, (struct sockaddr *) &address, addrlen); } fput_light(sock->file, fput_needed); } return err; }
bind函数根据前面返回的fd,找到创建的socket, 将 sockaddr 从用户态拷贝到内核态。调用对应的bind接口。 TCP对应bind函数就是inet_bind
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int inet_bind (struct socket *sock, struct sockaddr *uaddr, int addr_len) { struct sockaddr_in *addr = (struct sockaddr_in *)uaddr; struct sock *sk = sock->sk; struct inet_sock *inet = inet_sk(sk); struct net *net = sock_net(sk); unsigned short snum; ...... snum = ntohs(addr->sin_port); ...... inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr; if ((snum || !inet->bind_address_no_port) && sk->sk_prot->get_port(sk, snum)) { ...... } inet->inet_sport = htons(inet->inet_num); inet->inet_daddr = 0 ; inet->inet_dport = 0 ; sk_dst_reset(sk); }
这里主要就是根据传入的端口号地址,来检查端口是否冲突,是否可以绑定。然后设置本端的端口和地址,对端的端口和地址设置为0, 后面连接建立后再赋值。
listen函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SYSCALL_DEFINE2(listen, int , fd, int , backlog) { struct socket *sock ; int err, fput_needed; int somaxconn; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn; if ((unsigned int )backlog > somaxconn) backlog = somaxconn; err = sock->ops->listen(sock, backlog); fput_light(sock->file, fput_needed); } return err; }
listen 函数跟bind是一个模子的,根据fd获取到socket,然后调用对应的listen函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int inet_listen (struct socket *sock, int backlog) { struct sock *sk = sock->sk; unsigned char old_state; int err; old_state = sk->sk_state; if (old_state != TCP_LISTEN) { err = inet_csk_listen_start(sk, backlog); } sk->sk_max_ack_backlog = backlog; }
如果这个 socket 还不在 TCP_LISTEN 状态,会调用 inet_csk_listen_start 进入监听状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int inet_csk_listen_start (struct sock *sk, int backlog) { struct inet_connection_sock *icsk = inet_csk(sk); struct inet_sock *inet = inet_sk(sk); int err = -EADDRINUSE; reqsk_queue_alloc(&icsk->icsk_accept_queue); sk->sk_max_ack_backlog = backlog; sk->sk_ack_backlog = 0 ; inet_csk_delack_init(sk); sk_state_store(sk, TCP_LISTEN); if (!sk->sk_prot->get_port(sk, inet->inet_num)) { ...... } ...... }
建立了一个新的结构 inet_connection_sock,这个结构一开始是 struct inet_sock,inet_csk 其实做了一次强制类型转换,扩大了结构.
struct inet_connection_sock 结构比较复杂。如果打开它,你能看到处于各种状态的队列,各种超时时间、拥塞控制等字眼。我们说 TCP 是面向连接的,就是客户端和服务端都是有一个结构维护连接的状态,就是指这个结构。
在内核中,为每个 Socket 维护两个队列。一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于 established 状态;一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于 syn_rcvd 的状态。
这里把改用的队列创建起来后,状态置成Listen。
accetp函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 SYSCALL_DEFINE3(accept, int , fd, struct sockaddr __user *, upeer_sockaddr, int __user *, upeer_addrlen) { return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0 ); } SYSCALL_DEFINE4(accept4, int , fd, struct sockaddr __user *, upeer_sockaddr, int __user *, upeer_addrlen, int , flags) { struct socket *sock , *newsock ; struct file *newfile ; int err, len, newfd, fput_needed; struct sockaddr_storage address ; ...... sock = sockfd_lookup_light(fd, &err, &fput_needed); newsock = sock_alloc(); newsock->type = sock->type; newsock->ops = sock->ops; newfd = get_unused_fd_flags(flags); newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name); err = sock->ops->accept(sock, newsock, sock->file->f_flags, false ); if (upeer_sockaddr) { if (newsock->ops->getname(newsock, (struct sockaddr *)&address, &len, 2 ) < 0 ) { } err = move_addr_to_user(&address, len, upeer_sockaddr, upeer_addrlen); } fd_install(newfd, newfile); ...... }
跟前面的listen、bind一样。找到socket结构,然后调用accept函数。这里根据旧的socket新建了一个newsock和新的newfd。同时也关联了一个file。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 int inet_accept (struct socket *sock, struct socket *newsock, int flags, bool kern) { struct sock *sk1 = sock->sk; int err = -EINVAL; struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err, kern); sock_rps_record_flow(sk2); sock_graft(sk2, newsock); newsock->state = SS_CONNECTED; } struct sock *inet_csk_accept (struct sock *sk, int flags, int *err, bool kern) { struct inet_connection_sock *icsk = inet_csk(sk); struct request_sock_queue *queue = &icsk->icsk_accept_queue; struct request_sock *req ; struct sock *newsk ; int error; if (sk->sk_state != TCP_LISTEN) goto out_err; if (reqsk_queue_empty(queue )) { long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK); error = inet_csk_wait_for_connect(sk, timeo); } req = reqsk_queue_remove(queue , sk); newsk = req->sk; ...... } static int inet_csk_wait_for_connect (struct sock *sk, long timeo) { struct inet_connection_sock *icsk = inet_csk(sk); DEFINE_WAIT(wait); int err; for (;;) { prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); release_sock(sk); if (reqsk_queue_empty(&icsk->icsk_accept_queue)) timeo = schedule_timeout(timeo); sched_annotate_sleep(); lock_sock(sk); err = 0 ; if (!reqsk_queue_empty(&icsk->icsk_accept_queue)) break ; err = -EINVAL; if (sk->sk_state != TCP_LISTEN) break ; err = sock_intr_errno(timeo); if (signal_pending(current)) break ; err = -EAGAIN; if (!timeo) break ; } finish_wait(sk_sleep(sk), &wait); return err; }
如果 icsk_accept_queue 为空,则调用 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。
这个新的newsock的状态最终为CONNECTED, 就可以用来发送和接收数据了。
connect 函数 1 2 3 4 5 6 7 8 9 10 11 SYSCALL_DEFINE3(connect, int , fd, struct sockaddr __user *, uservaddr, int , addrlen) { struct socket *sock ; struct sockaddr_storage address ; int err, fput_needed; sock = sockfd_lookup_light(fd, &err, &fput_needed); err = move_addr_to_kernel(uservaddr, addrlen, &address); err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags); }
connect函数是发起三次握手的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags, int is_sendmsg) { struct sock *sk = sock->sk; int err; long timeo; switch (sock->state) { ...... case SS_UNCONNECTED: err = -EISCONN; if (sk->sk_state != TCP_CLOSE) goto out; err = sk->sk_prot->connect(sk, uaddr, addr_len); sock->state = SS_CONNECTING; break ; } timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) { ...... if (!timeo || !inet_wait_for_connect(sk, timeo, writebias)) goto out; err = sock_intr_errno(timeo); if (signal_pending(current)) goto out; } sock->state = SS_CONNECTED; } int tcp_v4_connect (struct sock *sk, struct sockaddr *uaddr, int addr_len) { struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; struct inet_sock *inet = inet_sk(sk); struct tcp_sock *tp = tcp_sk(sk); __be16 orig_sport, orig_dport; __be32 daddr, nexthop; struct flowi4 *fl4 ; struct rtable *rt ; ...... orig_sport = inet->inet_sport; orig_dport = usin->sin_port; rt = ip_route_connect(fl4, nexthop, inet->inet_saddr, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, IPPROTO_TCP, orig_sport, orig_dport, sk); ...... tcp_set_state(sk, TCP_SYN_SENT); err = inet_hash_connect(tcp_death_row, sk); sk_set_txhash(sk); rt = ip_route_newports(fl4, rt, orig_sport, orig_dport, inet->inet_sport, inet->inet_dport, sk); sk->sk_gso_type = SKB_GSO_TCPV4; sk_setup_caps(sk, &rt->dst); if (likely(!tp->repair)) { if (!tp->write_seq) tp->write_seq = secure_tcp_seq(inet->inet_saddr, inet->inet_daddr, inet->inet_sport, usin->sin_port); tp->tsoffset = secure_tcp_ts_off(sock_net(sk), inet->inet_saddr, inet->inet_daddr); } rt = NULL ; ...... err = tcp_connect(sk); ...... }
这里选定源地址和端口, 然后设置目标地址和端口,下面就是修改一下socket的状态,然后发送报文。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int tcp_connect (struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *buff ; int err; ...... tcp_connect_init(sk); ...... buff = sk_stream_alloc_skb(sk, 0 , sk->sk_allocation, true ); ...... tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); tcp_mstamp_refresh(tp); tp->retrans_stamp = tcp_time_stamp(tp); tcp_connect_queue_skb(sk, buff); tcp_ecn_send_syn(sk, buff); err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) : tcp_transmit_skb(sk, buff, 1 , sk->sk_allocation); ...... tp->snd_nxt = tp->write_seq; tp->pushed_seq = tp->write_seq; buff = tcp_send_head(sk); if (unlikely(buff)) { tp->snd_nxt = TCP_SKB_CB(buff)->seq; tp->pushed_seq = TCP_SKB_CB(buff)->seq; } ...... inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX); return 0 ; }
struct tcp_sock 里面维护了更多的 TCP 的状态. 接下来 tcp_init_nondata_skb 初始化一个 SYN 包,tcp_transmit_skb 将 SYN 包发送出去,inet_csk_reset_xmit_timer 设置了一个 timer,如果 SYN 发送不成功,则再次发送。
在调用 sk->sk_prot->connect 之后,inet_wait_for_connect 会一直等待客户端收到服务端的 ACK。三次握手,实际就是实现了一个状态机,不同状态收到不同报文,作不同的处理。
这个和我今年初写的modbus的那个状态机实现就很像了,架构都差不多。C语言层层回调,最后核心代码都是同一个。
TCP接收到包后调用tcp_rcv_state_process
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int tcp_rcv_state_process (struct sock *sk, struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); struct inet_connection_sock *icsk = inet_csk(sk); const struct tcphdr *th = tcp_hdr(skb); struct request_sock *req ; int queued = 0 ; bool acceptable; switch (sk->sk_state) { ...... case TCP_LISTEN: ...... if (th->syn) { acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0 ; if (!acceptable) return 1 ; consume_skb(skb); return 0 ; } ...... }
不同类型的socket 有对应的结构保存处理函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const struct inet_connection_sock_af_ops ipv4_specific = { .queue_xmit = ip_queue_xmit, .send_check = tcp_v4_send_check, .rebuild_header = inet_sk_rebuild_header, .sk_rx_dst_set = inet_sk_rx_dst_set, .conn_request = tcp_v4_conn_request, .syn_recv_sock = tcp_v4_syn_recv_sock, .net_header_len = sizeof (struct iphdr), .setsockopt = ip_setsockopt, .getsockopt = ip_getsockopt, .addr2sockaddr = inet_csk_addr2sockaddr, .sockaddr_len = sizeof (struct sockaddr_in), .mtu_reduced = tcp_v4_mtu_reduced, };
咻咻咻,that it is. 主要函数就是这些了。
更多 其实代码都不是很高深,只要花点时间深入进去,都可以看出个七八分,流程都可以梳理通。然后,对照网络协议的具体细节,就可以更深入的了解了,一万小时真香定律。
C语言经常会用到类型的强制转换,不同的结构体的头部是相同类型的,然后后面是扩展的数据。这个有点像面向对象的父类子类, 嗯 。特别在协议解析和处理的代码中,看到的就很多。最常见的就是TLV结构,前面是类型和长度,后面是具体的数据,根据前面的类型,我们就知道后面数据的结构了。
行动,才不会被动!
欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。