socket从创建到连接过程小结

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

socket()

系统调用流程图

aW8ts0.png

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
2
3
4
5
6
7
8
9
10
11
struct socket {
socket_state state;//枚举类型 标识了当前socket的状态 eg. SS_CONNECTED, SS_DISCONNECTED
kmemcheck_bitfield_begin(type);
short type;//标识套接字类型 eg.SOCK_STREAM,SOCK_DGRAM
kmemcheck_bitfield_end(type);
unsigned long flags;
struct socket_wq __rcu *wq;
struct file *file;//与套接字对应的文件
struct sock *sk;//指向代表下层协议(network layer)数据的sock结构
const struct proto_ops *ops;
};

sock_alloc()

先分配socketstruct socket *sock_alloc(void)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* sock_alloc - allocate a socket
*分配inode,socket 这两者是一一对应的
*/
static struct socket *sock_alloc(void)
{
struct inode * inode;
struct socket * sock;

inode = new_inode(sock_mnt->mnt_sb);//分配inode
sock = SOCKET_I(inode);//通过inode得到socket

inode->i_mode = S_IFSOCK|S_IRWXUGO;
inode->i_sock = 1;
inode->i_uid = current->fsuid;
inode->i_gid = current->fsgid;

get_cpu_var(sockets_in_use)++;
put_cpu_var(sockets_in_use);
return sock;
}

net_families->create()

从全局net_families数组中根据下标family取到对应的struct net_proto_family结构pf,然后调用create() 对于ipv4协议而言,对应到net/ipv4/Af_inet.cinet_create()

1
2
if ((err = net_families[family]->create(sock, protocol)) < 0)
goto out_module_put;

struct net_proto_family定义如下:

1
2
3
4
5
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};

inet_create

  1. 先将struct socket的state设为SS_UNCONNECTED;

  2. 根据struct socket的type(eg.SOCK_STREAM), 遍历inetsw[type], 找到对应到protocol的结构体

    struct inet_protosw定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* This is used to register socket interfaces for IP protocols.  */
struct inet_protosw {
struct list_head list;

/* These two fields form the lookup key. */
unsigned short type; /* This is the 2nd argument to socket(2). */
int protocol; /* This is the L4 protocol number. */

struct proto *prot;
struct proto_ops *ops;

int capability; /* Which (if any) capability do
* we need to use this socket
* interface?
*/
char no_check; /* checksum on rcv/xmit/none? */
unsigned char flags; /* See INET_PROTOSW_* below. */
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int inet_create(struct socket *sock, int protocol)
{
struct inet_protosw *answer;
struct list_head *p;
sock->state = SS_UNCONNECTED;//1.
/* Look for the requested type/protocol pair. */
list_for_each_rcu(p, &inetsw[sock->type]) { //2.inetsw是一个链表数组, key为SOCK_STREAM, SOCK_DGRAM, SOCK_RAW等等.
answer = list_entry(p, struct inet_protosw, list);
}
sock->ops = answer->ops;//3.
sk = sk_alloc(PF_INET, GFP_KERNEL,answer_prot->slab_obj_size, answer_prot->slab);//4.
inet = inet_sk(sk);//5.
sock_init_data(sock, sk);//6.
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);//7.
if (err)
sk_common_release(sk);
}

}
  1. 将”对应到protocol的结构体”的ops赋给struct socket结构的ops. sock->ops = answer->ops;

  2. 调用sk_alloc, 分配网络子系统核心(net/core)的数据结构struct sock ( 记录family, protocol到sk_family, sk_prot成员 )

  3. 将struct sock强转为struct inet_sk(调用inet_sk)

  4. 调用sock_init_data(struct socket, struct sock),用scoket 来初始化sock。

  5. 调用sk->sk_prot->init, 例如对于TCP, 指向net/ipv4/Tcp_ipv4.c的全局结构体struct proto tcp_port中的tcp_v4_init_sock, 此方法完成该socket在内核网络子系统TCP层的初始化:

sock_map_fd

1
2
3
4
5
6
7
8
9
10
11
12
13
// net/socket.c
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
int fd = get_unused_fd_flags(flags);//1.
...
newfile = sock_alloc_file(sock, flags, NULL);//2.
if (likely(!IS_ERR(newfile))) {
fd_install(fd, newfile);//3.
return fd;
}
...
}

sock_map_fd()主要用于对socket的file指针初始化,经过sock_map_fd()操作后,socket就通过其file指针与VFS管理的文件进行了关联,便可以进行文件的各种操作,如read、write、lseek。

sock_map_fd流程如下

  1. 找到一个未使用的文件描述符fd。

  2. 为socket分配一个struct file实例。

  3. 建立fd到socket file的映射关系,并返回fd给上层。

将struct socket的file设为struct file,struct file的private_data设为struct socket;这样struct socket和struct file便互相关联起来了.

aW8YMq.png

bind()

image-20200807150150809

bind()作用是给创建的套接字绑定地址,函数原型以及包含的头文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <sys/types.h>       
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//sockaddr 结构体
struct sockaddr {
sa_family_t sa_family;// 地址协议族
char sa_data[14];
}
//sockaddr_in
struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};

bind()中的第二个参数addr类型为struct sockaddr,在实际的socket编程中,我们一般都是将特定类型地址转化为sockaddr,比如将ipv4中绑定地址一般如下操作

1
2
3
4
5
6
7
8
9
10
struct sockaddr_in servaddr; //地址格式描述
memset(&servaddr, 0, sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET; //IPV4
servaddr.sin_port = htons(123); //端口号
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY一般为0,内核选择IP地址

if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr_in)) == -1) {
printf("bind socket addr failed!\n");
return 0;
}

既有sockaddr,又有sockaddr_in,然后还要转化,看起来多此一举,其实不然,不同的协议族地址可能不一样(eg.ipv6就和ipv4不一样),sockaddr提供了一个统一的地址接口。

sys_bind

与socket()一样,bind通过系统调用统一接口到了sys_bind

sockfd_lookup_light

sockfd_lookup_light通过fd来查找sock

  1. 在当前进程的描述符中通过fd 找到struct file
  2. 通过file->f_dentry->d_inode 得到inode
  3. 通过 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
2
3
4
5
6
7
8
define container_of(ptr, type, member) ({			
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );})

struct socket_alloc {
struct socket socket;
struct inode vfs_inode;
};
  • move_addr_to_kernel 将地址拷贝到内核空间
  • ops->bind; 这个是sys_bind最为重要的一步调用,在创建套接字时候,我们将sock->ops设定为了与协议类型相关的函数操作集,这里我们具体分析tcp,在TCP协议情况下inet_stream_ops中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
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 = security_socket_bind(sock,
(struct sockaddr *)&address,
addrlen);
if (!err)
err = sock->ops->bind(sock,
(struct sockaddr *)
&address, addrlen);//inet_bind
}
fput_light(sock->file, fput_needed);
}
return err;
}

inet_bind

  1. 做了一些检查,比如绑定地址的长度,协议族类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*地址长度检验*/
if (addr_len < sizeof(struct sockaddr_in))
goto out;
/*bind地址中协议检查,必须是下面两种情况
* 1.绑定的地址协议为AF_INET
* 2.绑定协议为0(AF_UNSPEC)同时地址也为0
* 否则直接退出inet_bind ,返回地址不支持错误码
*/
if (addr->sin_family != AF_INET) {
/* Compatibility games : accept AF_UNSPEC (mapped to AF_INET)
* only if s_addr is INADDR_ANY.
*/
err = -EAFNOSUPPORT;
if (addr->sin_family != AF_UNSPEC ||
addr->sin_addr.s_addr != htonl(INADDR_ANY))
goto out;
}
  1. inet_addr_type(net, addr->sin_addr.s_addr); 做了地址类型检查, 地址类型必须是本机,多播,组播中的一个,否则直接返回错误码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*获取根据IP地址得出地址类型
RTN_LOCAL 本机地址
RTN_MULTICAST 多播
RTN_BROADCAST 广播
RTN_UNICAST
*/
chk_addr_ret = inet_addr_type(net, addr->sin_addr.s_addr);

err = -EADDRNOTAVAIL;
if (!net->ipv4_sysctl_ip_nonlocal_bind &&
!(inet->freebind || inet->transparent) &&
addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
chk_addr_ret != RTN_LOCAL &&
chk_addr_ret != RTN_MULTICAST &&
chk_addr_ret != RTN_BROADCAST)
goto out;
  1. 获取端口号,并且对保留端口做访问权限检查。
1
2
3
4
5
6
7
8
9
snum = ntohs(addr->sin_port);
err = -EACCES;
/*
* 3.要绑定的端口小于1024时候,要求运行该应用程序的为超级权限
* 否则返回并报权限不运行的错误
*/
if (snum && snum < PROT_SOCK &&
!ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
goto out;
  1. bind动作发生在TCP三次握手之前,此时TCP状态应该是CLOSE且没有绑定过其他端口
1
2
if (sk->sk_state != TCP_CLOSE || inet->inet_num)
goto out_release_sock;
  1. 端口号能否被绑定检查,端口没有被使用返回0,否则返回非0。
1
2
3
4
5
6
//	
if (sk->sk_prot->get_port(sk, snum)) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
  1. 更新sk_userlocks标记,表明本地地址和端口已经绑定
1
2
3
4
5
6
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
if (inet->inet_rcv_saddr)
sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
if (snum)
sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
inet->inet_sport = htons(inet->inet_num);//端口绑定

connect()

客户端主动发起连接,调用connect

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

connect最后调用系统调用sys_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
asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
struct socket *sock;
char address[MAX_SOCK_ADDR];
int err;

sock = sockfd_lookup(fd, &err);
if (!sock)
goto out;
err = move_addr_to_kernel(uservaddr, addrlen, address);
if (err < 0)
goto out_put;

err = security_socket_connect(sock, (struct sockaddr *)address, addrlen);
if (err)
goto out_put;

err = sock->ops->connect(sock, (struct sockaddr *) address, addrlen,
sock->file->f_flags);
out_put:
sockfd_put(sock);
out:
return err;
}

查找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.

流程图

ahrMnO.png

  1. 调用ip_route_connect进行寻路, 取得struct rtable,目的是为了确定下一跳目的地址
1
2
3
4
5
6
tmp = ip_route_connect(&rt, nexthop, inet->saddr,
RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
IPPROTO_TCP,
inet->sport, usin->sin_port, sk);
if (!inet->opt || !inet->opt->srr)
daddr = rt->rt_dst;
  1. 设置套接字状态为SYN_SENT
  2. 调用tcp_v4_hash_connect(sk)

这一步的目的是绑定端口,并且将套接字插入到bind链表,哈希链表以port为关键字,将地址相同的串在一个链表上(哈希冲突)。

这一步之前source port可能还为0(对于client端调用socket后直接调用connect不经过bind的情况, source port为0).所以该方法会生成一个随机的source port, 赋给struct inet_sk的inet_sport成员

  1. ip_route_newports 分配完端口后,再查一次路由
  2. 生成初始序列号ISN
1
secure_tcp_sequence_number(inet->saddr,inet->daddr,inet->sport,usin->sin_port);
  1. tcp_connect()构造SYN,将连接数据包发送出去
  • buff = alloc_skb(MAX_TCP_HEADER + 15, sk->sk_allocation); 分配skb
  • TCP_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static long inet_wait_for_connect(struct sock *sk, long timeo)
{
DEFINE_WAIT(wait);

prepare_to_wait(sk->sk_sleep, &wait, TASK_INTERRUPTIBLE);

/* Basic assumption: if someone sets sk->sk_err, he _must_
* change state of the socket from TCP_SYN_*.
* Connect() does not allow to get error notifications
* without closing the socket.
*/
//循环检查状态变化
while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
release_sock(sk);
timeo = schedule_timeout(timeo);
lock_sock(sk);
if (signal_pending(current) || !timeo)
break;
prepare_to_wait(sk->sk_sleep, &wait, TASK_INTERRUPTIBLE);
}
finish_wait(sk->sk_sleep, &wait);
return 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
2
3
4
5
6
7
8
9
10
11
12
case TCP_SYN_SENT:
tp->rx_opt.saw_tstamp = 0;
tcp_mstamp_refresh(tp);
//进入tcp_rcv_synsent_state_process处理
queued = tcp_rcv_synsent_state_process(sk, skb, th);
if (queued >= 0)
return queued;
/* Do step6 onward by hand. */
tcp_urg(sk, skb, th);
__kfree_skb(skb);
tcp_data_snd_check(sk);
retur

在tcp_rcv_synsent_state_process中

1
2
3
4
5
6
7
8
//检查ACK的有效性
tcp_ack(sk, skb, FLAG_SLOWPATH);
...
//如果ack有效,则完成连接,将状态兄TCP_SYN_SENT->TCP_ESTABLISHED
tcp_finish_connect(sk, skb);
...
//发送ack
tcp_send_ack(sk);

tcp_finish_connect将连接状态设为TCP_ESTABLISHED,然后唤醒之前阻塞在inet_wait_for_connect() 的进程,至此connet()完成。

1
2
3
4
5
6
7
8
9
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
...
tcp_set_state(sk, TCP_ESTABLISHED); /* 在这里设置为连接已建立的状态 */
...
if (! sock_flag(sk, SOCK_DEAD)) {
sk->sk_state_change(sk); /* 指向sock_def_wakeup,会唤醒调用connect()的进程,完成连接的建立 */
sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT); /* 如果使用了异步通知,则发送SIGIO通知进程可写 */
}

参考

What’s the difference between sockaddr, sockaddr_in, and sockaddr_in6?

bind

Linux内核网络子系统源码分析(2) – connect系统调用

Socket层实现系列 - connect()的实现_zhangskd的专栏-CSDN博客_inet_csk_wait_for_connect

socket建立连接 sys_connect