分析了套接字创建到连接过程中,一些关键的函数调用以及数据结构,同时分析了connect()过程中tcp三次握手状态变化。
socket()
系统调用流程图

sock_create()
1 | sock_create(family, type, protocol, &sock) |
family
指定了协议族,比较常用的ipv4协议族对应AF_INET,PF_INET。
AF_INET 表示address_family, PF_INET 表示protocol family ,这两者的宏定义是相同的,对于BSD,是AF,对于POSIX是PF。
| 名称 | 含义 | 名称 | 含义 |
|---|---|---|---|
| PF_UNIX,PF_LOCAL | 本地通信 | PF_X25 | ITU-T X25 / ISO-8208协议 |
| AF_INET,PF_INET | IPv4 Internet协议 | PF_AX25 | Amateur radio AX.25 |
| PF_INET6 | IPv6 Internet协议 | PF_ATMPVC | 原始ATM PVC访问 |
| PF_IPX | IPX-Novell协议 | PF_APPLETALK | Appletalk |
| PF_NETLINK | 内核用户界面设备 |
type
用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK_DGRAM(数据包套接字)等。
protocol
protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0。
因此对于一个面向对象的连接,创建一个套接字如下
int lfd=socket(AF_INET,SOCK_STREAM,0);
sock 类型是struct socket
1 | struct socket { |
sock_alloc()
先分配socketstruct socket *sock_alloc(void)
1 | /** |
net_families->create()
从全局net_families数组中根据下标family取到对应的struct net_proto_family结构pf,然后调用create() 对于ipv4协议而言,对应到net/ipv4/Af_inet.c的inet_create()。
1 | if ((err = net_families[family]->create(sock, protocol)) < 0) |
struct net_proto_family定义如下:
1 | static const struct net_proto_family inet_family_ops = { |
inet_create
先将struct socket的state设为SS_UNCONNECTED;
根据struct socket的type(eg.SOCK_STREAM), 遍历inetsw[type], 找到对应到protocol的结构体
struct inet_protosw定义如下
1 | /* This is used to register socket interfaces for IP protocols. */ |
1 | static int inet_create(struct socket *sock, int protocol) |
将”对应到protocol的结构体”的ops赋给struct socket结构的ops.
sock->ops = answer->ops;调用sk_alloc, 分配网络子系统核心(net/core)的数据结构struct sock ( 记录family, protocol到sk_family, sk_prot成员 )
将struct sock强转为struct inet_sk(调用inet_sk)
调用sock_init_data(struct socket, struct sock),用scoket 来初始化sock。
调用sk->sk_prot->init, 例如对于TCP, 指向net/ipv4/Tcp_ipv4.c的全局结构体struct proto tcp_port中的tcp_v4_init_sock, 此方法完成该socket在内核网络子系统TCP层的初始化:
sock_map_fd
1 | // net/socket.c |
sock_map_fd()主要用于对socket的file指针初始化,经过sock_map_fd()操作后,socket就通过其file指针与VFS管理的文件进行了关联,便可以进行文件的各种操作,如read、write、lseek。
sock_map_fd流程如下
找到一个未使用的文件描述符fd。
为socket分配一个struct file实例。
建立fd到socket file的映射关系,并返回fd给上层。
将struct socket的file设为struct file,struct file的private_data设为struct socket;这样struct socket和struct file便互相关联起来了.

bind()

bind()作用是给创建的套接字绑定地址,函数原型以及包含的头文件如下
1 |
|
bind()中的第二个参数addr类型为struct sockaddr,在实际的socket编程中,我们一般都是将特定类型地址转化为sockaddr,比如将ipv4中绑定地址一般如下操作
1 | struct sockaddr_in servaddr; //地址格式描述 |
既有sockaddr,又有sockaddr_in,然后还要转化,看起来多此一举,其实不然,不同的协议族地址可能不一样(eg.ipv6就和ipv4不一样),sockaddr提供了一个统一的地址接口。
sys_bind
与socket()一样,bind通过系统调用统一接口到了sys_bind
sockfd_lookup_light
sockfd_lookup_light通过fd来查找sock
- 在当前进程的描述符中通过fd 找到struct file
- 通过
file->f_dentry->d_inode得到inode - 通过
SOCKET_I(inode)得到sock
这里具体看下如何通过inode得到sock。
SOCKET_I通过调用container_of(inode, struct socket_alloc, vfs_inode)->socket, vfs_inode类型为inode,作为socket_alloc的数据成员。
container_of的作用就是通过传入inode地址,inode地址作为socket_alloc的第二个数据成员,通过计算相对socket_alloc首地址的偏移,既可以获得socket_alloc地址,即socket地址。
1 | define container_of(ptr, type, member) ({ |
- move_addr_to_kernel 将地址拷贝到内核空间
- ops->bind; 这个是sys_bind最为重要的一步调用,在创建套接字时候,我们将sock->ops设定为了与协议类型相关的函数操作集,这里我们具体分析tcp,在TCP协议情况下inet_stream_ops中bind成员函数为inet_bind。
1 | SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen) |
inet_bind
- 做了一些检查,比如绑定地址的长度,协议族类型。
1 | /*地址长度检验*/ |
- inet_addr_type(net, addr->sin_addr.s_addr); 做了地址类型检查, 地址类型必须是本机,多播,组播中的一个,否则直接返回错误码
1 | /*获取根据IP地址得出地址类型 |
- 获取端口号,并且对保留端口做访问权限检查。
1 | snum = ntohs(addr->sin_port); |
- bind动作发生在TCP三次握手之前,此时TCP状态应该是CLOSE且没有绑定过其他端口
1 | if (sk->sk_state != TCP_CLOSE || inet->inet_num) |
- 端口号能否被绑定检查,端口没有被使用返回0,否则返回非0。
1 | // |
- 更新
sk_userlocks标记,表明本地地址和端口已经绑定
1 | inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr; |
connect()
客户端主动发起连接,调用connect
1 | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
connect最后调用系统调用sys_connect
1 | asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) |
查找sock,将地址从用户空间拷贝内核空间,这些操作与bind()分析一样, 这里不再赘述,主要看下 sock->ops->connect
对于ipv4这个family来说,type为SOCK_STREAM,sock->ops指向inet_stream_ops,sock->ops->connect指向inet_stream_ops的connect, 即inet_stream_connect.
流程图

- 调用ip_route_connect进行寻路, 取得struct rtable,目的是为了确定下一跳目的地址
1 | tmp = ip_route_connect(&rt, nexthop, inet->saddr, |
- 设置套接字状态为SYN_SENT
- 调用
tcp_v4_hash_connect(sk)
这一步的目的是绑定端口,并且将套接字插入到bind链表,哈希链表以port为关键字,将地址相同的串在一个链表上(哈希冲突)。
这一步之前source port可能还为0(对于client端调用socket后直接调用connect不经过bind的情况, source port为0).所以该方法会生成一个随机的source port, 赋给struct inet_sk的inet_sport成员
ip_route_newports分配完端口后,再查一次路由- 生成初始序列号ISN
1 | secure_tcp_sequence_number(inet->saddr,inet->daddr,inet->sport,usin->sin_port); |
tcp_connect()构造SYN,将连接数据包发送出去
buff = alloc_skb(MAX_TCP_HEADER + 15, sk->sk_allocation);分配skbTCP_SKB_CB(buff)->flags = TCPCB_FLAG_SYN;设置syn 标志__skb_queue_tail(&sk->sk_write_queue, buff);将构建好的skb添加到套接字对应的写队列tcp_transmit_skb(sk, skb_clone(buff, GFP_KERNEL));tcp层实际的处理和传输过程tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto);设定syn超时重传
我们说connect() 调用时候发送SYN, 在返回之前回复对方ACK ,在代码中体现?
客户端发送syn之后,阻塞在inet_wait_for_connect()
1 | static long inet_wait_for_connect(struct sock *sk, long timeo) |
客户端在收到SYN+ACK之后,调用流程
1 | tcp_v4_rcv→tcp_v4_do_rcv→tcp_rcv_state_process→tcp_rcv_synsent_state_process |
在函数tcp_rcv_state_process中
1 | case TCP_SYN_SENT: |
在tcp_rcv_synsent_state_process中
1 | //检查ACK的有效性 |
tcp_finish_connect将连接状态设为TCP_ESTABLISHED,然后唤醒之前阻塞在inet_wait_for_connect() 的进程,至此connet()完成。
1 | void tcp_finish_connect(struct sock *sk, struct sk_buff *skb) |
参考
What’s the difference between sockaddr, sockaddr_in, and sockaddr_in6?
Linux内核网络子系统源码分析(2) – connect系统调用
Socket层实现系列 - connect()的实现_zhangskd的专栏-CSDN博客_inet_csk_wait_for_connect