0%

Linux网络内核源码分析|套接字之收发数据、监听、连接、绑定

INET套接字类型

  • SOCK_STREAM:采用TCP协议服务,提供可靠的双工顺序数据流。
  • SOCK_DGRAM:采用UDP协议服务,提供双工数据传送,不保证不丢失、不错序。
  • SOCK_RAW:未采用TCP/UDP协议,应用程序可通过该套接字与IP层交互数据。

关于套接字传输数据的流程见附页流程图。

收发数据

发送数据

调用过程

当通信双方需要想对方发送/写数据时,可以调用send()、sendto()、write()函数。

send()|write()|sendto() → sys_send() → sys_sendto() → sock_sendmsg() → __sock_sendmsg() → sock->prot->sendmsg() → sk->sk_prot->sendmsg()

如果是SOCK_STREAM套接字,则最后两步是→ inet_sendmsg() → tcp_sendmsg()

net/socket.c

sys_send()

包装了sys_sendto()函数。

sys_sendto()

向指定地址发送一个数据包:完成发送数据前的准备工作,把用户空间地址转换成内核空间地址,检查用户空间数据区是否可读,调用sock_sendmsg()函数指定具体的发送操作。

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
//根据协议类型执行发送数据的操作
asmlinkage long sys_sendto(int fd, void __user * buff, size_t len, unsigned flags,
struct sockaddr __user *addr, int addr_len)
{
struct socket *sock;
char address[MAX_SOCK_ADDR];
int err;
struct msghdr msg;
struct iovec iov;

//根据套接字描述符fd查找对应的socket结构体
sock = sockfd_lookup(fd, &err);
if (!sock)
goto out;
iov.iov_base=buff; //指向等待发送的数据内容缓冲区
iov.iov_len=len;
// msg用来记录数据的发送信息
msg.msg_name=NULL;
msg.msg_iov=&iov;
msg.msg_iovlen=1;
msg.msg_control=NULL;
msg.msg_controllen=0;
msg.msg_namelen=0;
if(addr)
{//把用户空间地址转换成内核空间地址
err = move_addr_to_kernel(addr, addr_len, address);
if (err < 0)
goto out_put;
msg.msg_name=address;
msg.msg_namelen=addr_len;
}
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;
//执行具体的套接字发送操作
err = sock_sendmsg(sock, &msg, len);

out_put:
sockfd_put(sock);
out:
return err;
}

sock_sendmsg()

调用__sock_sendmsg()发送数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
struct kiocb iocb;
struct sock_iocb siocb;
int ret;

init_sync_kiocb(&iocb, NULL);
iocb.private = &siocb;
//调用__sock_sendmsg()发送数据
ret = __sock_sendmsg(&iocb, sock, msg, size);
if (-EIOCBQUEUED == ret)
ret = wait_on_sync_kiocb(&iocb);
return ret;
}

自此函数起,传递的参数多了一个struct kiocb

在Linux内核中,每个IO请求都对应一个kiocb结构体,其ki_filp成员指向对应的file指针,通过is_sync_kiocb可以判断某Kiocb是否为同步IO请求,真——同步IO请求,非真——异步IO请求。

块设备和网络设备本身就是异步的。只有字符设备驱动必须明确指出应支持AIO.需要说明的是AIO对于大多数字符设备而言都不是必须的。只有少数才需要。

参考博客:Linux内核开发之异步通知与异步I/O

__sock_sendmsg()

调用属于某协议族的套接字发送函数——在已指定的协议族操作集中。

1
2
3
4
5
6
7
8
9
static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock, 
struct msghdr *msg, size_t size)
{
//...
//创建套接字时,所指定的具体协议族操作集记录在sock->ops中。
//协议族操作集中发送数据的函数则为sock->ops->sendmsg。
//sock->ops->sendmsg指向具体协议族操作集中注册的函数
return sock->ops->sendmsg(iocb, sock, msg, size);
}

根据协议类型.type在struct inet_protosw inetsw_array[]查询INET协议具体的操作集。如果套接字为SOCK_STREAM,则.ops=&inet_stream_ops,在inet_stream_ops操作集中负责发送的.sendmsg=inet_sendmsg

因此,调用sock->ops->sendmsg(iocb, sock, msg, size);就等于调用inet_stream_ops操作集中的inet_sendmsg()函数。

关于inet_protosw inetsw_array数组,见我的博客:Linux网络内核源码分析-关于套接字

SOCK_STREAM套接字的sock->ops->sendmsg():inet_sendmsg()

net/ipv4/af_inet.c

1
2
3
4
5
6
7
8
9
10
11
12
int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
size_t size)
{
//得到套接字在传输层的struct sock表示结构
struct sock *sk = sock->sk;

/* We may need to bind the socket. */
if (!inet_sk(sk)->num && inet_autobind(sk))
return -EAGAIN;
//调用传输层的数据发送方法
return sk->sk_prot->sendmsg(iocb, sk, msg, size);
}

传输层的发送方法sk->sk_prot->sendmsg(): tcp_sendmsg()

net/ipv4/tcp.c

sk是struct socket sock中的指针,指向传输层套接字表示结构,sk中包含传输层协议操作集.prot成员。如果是SOCK_STREAM套接字,则.prot=&tcp_prot,对应sendmsg()的函数即tcp_sendmsg()。

接收数据

调用过程

当通信双方从对方接收/读数据时,可以调用read()、recv()、recvfrom()函数。

recv()|read()|recvfrom() → sys_recv() → sys_recvfrom() → sock_recvfrom() → __sock_recvmsg() → sock_common_recvmsg() → sock->ops->recvmsg() →sk->sk_prot->recvmsg()

sys_recv()

包装了sys_recvfrom()函数。

sys_recvfrom()

从一个套接字接收一个帧,有选择地记录发送方的地址,检查缓冲区是否可写,如果有需要,就把发送方地址从内核移到用户空间。

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
asmlinkage long sys_recvfrom(int fd, void __user * ubuf, size_t size, unsigned flags,
struct sockaddr __user *addr, int __user *addr_len)
{
struct socket *sock;
struct iovec iov;
struct msghdr msg;
char address[MAX_SOCK_ADDR];
int err,err2;
//根据套接字描述符fd找到套接字的struct socket结构
sock = sockfd_lookup(fd, &err);
if (!sock)
goto out;
msg.msg_control=NULL;
msg.msg_controllen=0;
msg.msg_iovlen=1;
msg.msg_iov=&iov;
iov.iov_len=size;
iov.iov_base=ubuf;
msg.msg_name=address;
msg.msg_namelen=MAX_SOCK_ADDR;
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
//调用sock_recvmsg执行具体的数据接收操作
err=sock_recvmsg(sock, &msg, size, flags);
if(err >= 0 && addr != NULL)
{
//将内核空间的网络数据复制到用户空间
err2=move_addr_to_user(address, msg.msg_namelen, addr, addr_len);
if(err2<0)
err=err2;
}
sockfd_put(sock);
out:
return err;
}

sock_recvmsg()

调用__sock_recvmsg()接收数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sock_recvmsg(struct socket *sock, struct msghdr *msg, 
size_t size, int flags)
{
struct kiocb iocb;
struct sock_iocb siocb;
int ret;

init_sync_kiocb(&iocb, NULL);
iocb.private = &siocb;
ret = __sock_recvmsg(&iocb, sock, msg, size, flags);
if (-EIOCBQUEUED == ret)
ret = wait_on_sync_kiocb(&iocb);
return ret;
}

__sock_recvmsg()

调用属于某协议族的套接字接收函数——在已指定的协议族操作集中。

1
2
3
4
5
6
7
8
9
static inline int __sock_recvmsg(struct kiocb *iocb, struct socket *sock, 
struct msghdr *msg, size_t size, int flags)
{
//...
//创建套接字时,所指定的具体协议族操作集记录在sock->ops中。
//协议族操作集中接收数据的函数则为sock->ops->recvmsg。
//sock->ops->sendmsg指向具体协议族操作集中注册的函数
return sock->ops->recvmsg(iocb, sock, msg, size, flags);
}

根据协议类型.type在struct inet_protosw inetsw_array[]查询INET协议具体的操作集。如果套接字为SOCK_STREAM,则.ops=&inet_stream_ops,在inet_stream_ops操作集中负责接收的.recvmsg=sock_common_recvmsg

因此,调用sock->ops->recvmsg(iocb, sock, msg, size);就等于调用inet_stream_ops操作集中的sock_common_recvmsg()函数。

SOCK_STREAM套接字的sock->ops->recvmsg():sock_common_recvmsg()

1
2
3
4
5
6
7
8
9
10
11
12
int sock_common_recvmsg(struct kiocb *iocb, struct socket *sock,struct msghdr *msg, size_t size, int flags)
{
//得到传输层协议套接字
struct sock *sk = sock->sk;
int addr_len = 0;
int err;
//调用传输层的数据接收函数
err = sk->sk_prot->recvmsg(iocb, sk, msg, size, flags & MSG_DONTWAIT,flags & ~MSG_DONTWAIT, &addr_len);
if (err >= 0)
msg->msg_namelen = addr_len;
return err;
}

根据套接字采用的传输层协议,sock_common_recvmsg()调用属于不同协议的接收函数。该函数在inet_create()时指定在inetsw_array中。

当套接字为SOCK_STREAM时,有.prot=&tcp_prot,操作集中.recvmsg=tcp_recvmsg。所以sock_common_recvmsg()调用sk->sk_prot->recvmsg就等于调用tcp_recvmsg()。

传输层的接收方法sk->sk_prot->recvmsg(): tcp_recvmsg()

net/ipv4/tcp.c

监听套接字

调用过程

listen() → sys_listen()

调用listen()函数,程序触发内核的sys_listen()函数,

sys_listen()

net/socket.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
asmlinkage long sys_listen(int fd, int backlog)
{
struct socket *sock;
int err;
//通过套接字描述符找到套接字对应的struct socket结构
if ((sock = sockfd_lookup(fd, &err)) != NULL) {
if ((unsigned) backlog > sysctl_somaxconn)
backlog = sysctl_somaxconn;

err = security_socket_listen(sock, backlog);
if (err) {
sockfd_put(sock);
return err;
}
//根据套接字协议族类型调用监听函数
err=sock->ops->listen(sock, backlog);
sockfd_put(sock);
}
return err;
}

如果是INET套接字,且为SOCK_STREAM类型,则调用sock->ops->listen()等于调用inet_listen()。

套接字连接

被动接收连接 accept()

accept() → sys_accept() → sock->ops->accept()

调用accept()函数时,程序触发sys_accept(),等待接收连接请求,如果允许连接,则重新创建一个代表该连接的套接字,并返回其套接字描述符。

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
asmlinkage long sys_accept(int fd, struct sockaddr __user *upeer_sockaddr, int __user *upeer_addrlen)
{
struct socket *sock, *newsock;
int err, len;
char address[MAX_SOCK_ADDR];
//通过套接字描述符找到套接字对应的socket结构
sock = sockfd_lookup(fd, &err);
if (!sock)
goto out;
//创建一个新的套接字
err = -EMFILE;
if (!(newsock = sock_alloc()))
goto out_put;
newsock->type = sock->type;
newsock->ops = sock->ops;
//根据套接字协议族类型调用接收连接请求的函数
//如果是INET协议套接字,则调用函数inet_accept
err = security_socket_accept(sock, newsock);
if (err)
goto out_release;
/*
* We don't need try_module_get here, as the listening socket (sock)
* has the protocol module (sock->ops->owner) held.
*/
__module_get(newsock->ops->owner);
//将地址信息从内核空间复制到用户空间
err = sock->ops->accept(sock, newsock, sock->file->f_flags);
if (err < 0)
goto out_release;

if (upeer_sockaddr) {
if(newsock->ops->getname(newsock, (struct sockaddr *)address, &len, 2)<0) {
err = -ECONNABORTED;
goto out_release;
}
err = move_addr_to_user(address, len, upeer_sockaddr, upeer_addrlen);
if (err < 0)
goto out_release;
}
/* File flags are not inherited via accept() unlike another OSes. */
if ((err = sock_map_fd(newsock)) < 0)
goto out_release;
security_socket_post_accept(sock, newsock);
out_put:
sockfd_put(sock);
out:
return err;
out_release:
sock_release(newsock);
goto out_put;
}

主动连接 connect()

connect() → sys_connect() → sock->ops->connect()

调用connect()发出连接请求,内核会启动函数sys_connect()。如果采用TCP协议,则将启动TCP三次握手过程。

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
asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
struct socket *sock;
char address[MAX_SOCK_ADDR];
int err;
//通过套接字描述符找到套接字的socket结构
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;
//根据套接字协议族类型调用连接请求函数
//如果是INET套接字,则调用函数inet_connect()
err = sock->ops->connect(sock, (struct sockaddr *) address, addrlen,
sock->file->f_flags);
out_put:
sockfd_put(sock);
out:
return err;
}

关闭连接 shutdown()

shutdown() → sys_shutdown() → sock->ops->shutdown()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
asmlinkage long sys_shutdown(int fd, int how)
{
int err;
struct socket *sock;
//通过套接字描述符找到套接字对应的socket结构
if ((sock = sockfd_lookup(fd, &err))!=NULL)
{
err = security_socket_shutdown(sock, how);
if (err) {
sockfd_put(sock);
return err;
}
//根据套接字协议族调用关闭套接字的函数
//如果是INET套接字,则调用函数inet_shutdown
err=sock->ops->shutdown(sock, how);
sockfd_put(sock);
}
return err;
}

绑定套接字

bind() → sys_bind()

net/socket.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
struct socket *sock;
char address[MAX_SOCK_ADDR];
int err;
//通过套接字描述符找到套接字对应的socket结构
if((sock = sockfd_lookup(fd,&err))!=NULL)
{
//将地址信息从用户空间复制到内核空间
if((err=move_addr_to_kernel(umyaddr,addrlen,address))>=0) {
err = security_socket_bind(sock, (struct sockaddr *)address, addrlen);
if (err) {
sockfd_put(sock);
return err;
}
//根据套接字协议族调用绑定地址的函数
//如果是INET套接字,则调用函数inet_bind()
err = sock->ops->bind(sock, (struct sockaddr *)address, addrlen);
}
sockfd_put(sock);
}
return err;
}

附:TCP/UDP数据传输流程