网络-内核是如何与用户进程交互
1、socket的直接创建
net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
...
retval = sock_create(family, type, protocol, &sock);
...
}
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
...
//分配socket对象
sock = sock_alloc();
...
//获取对应协议簇的操作表
pf = rcu_dereference(net_families[family]);
...
//调用协议簇的创建函数,对于AF_INET对应的是inet_create,这个函数在inet_init函数中被初始化
err = pf->create(net, sock, protocol, kern);
...
}
EXPORT_SYMBOL(__sock_create);
socket在内核中是怎么创建的?
sock_create->__sock_create->inet_create
在__sock_create里首先调用sock_alloc来分配一个struct sock内核对象,接着获取协议簇的操作函数表,调用其create方法。对于AF_INET协议簇来说,执行到的是inet_create函数。
static int inet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk; // 指向 sock 结构体,表示套接字
struct inet_protosw *answer; // 指向 inet_protosw 结构体,表示协议开关
struct inet_sock *inet; // 指向 inet_sock 结构体,表示 IPv4 套接字
struct proto *answer_prot; // 指向 proto 结构体,表示协议操作
unsigned char answer_flags; // 协议开关标志
int try_loading_module = 0; // 尝试加载模块的次数
int err; // 用于存储函数返回值
/*
* 检查协议号是否在有效范围内。
* 如果不在,返回 -EINVAL 错误。
*/
if (protocol < 0 || protocol >= IPPROTO_MAX)
return -EINVAL;
/*
* 初始化套接字状态为未连接。
*/
sock->state = SS_UNCONNECTED;
/*
* 查找请求的类型/协议对。
* 首先尝试在 RCU 读锁保护下查找协议。
*/
lookup_protocol:
err = -ESOCKTNOSUPPORT; // 设置错误为“不支持的套接字类型”
rcu_read_lock(); // 锁定 RCU 读锁
/*
* 遍历协议开关列表,查找匹配的协议。
* 如果找到匹配的协议,跳出循环。
*/
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT; // 设置错误为“不支持的协议”
}
/*
* 如果没有找到匹配的协议,尝试加载相应的模块。
* 如果模块加载后,再次尝试查找协议。
*/
if (unlikely(err)) {
if (try_loading_module < 2) {
rcu_read_unlock(); // 解锁 RCU 读锁
if (++try_loading_module == 1)
request_module("net-pf-%d-proto-%d-type-%d",
PF_INET, protocol, sock->type);
else
request_module("net-pf-%d-proto-%d",
PF_INET, protocol);
goto lookup_protocol; // 重新查找协议
} else {
goto out_rcu_unlock; // 如果模块加载失败,跳转到错误处理
}
}
/*
* 检查是否允许非内核进程创建原始套接字。
* 如果不允许,返回 -EPERM 错误。
*/
err = -EPERM;
if (sock->type == SOCK_RAW && !kern &&
!ns_capable(net->user_ns, CAP_NET_RAW))
goto out_rcu_unlock;
/*
* 设置套接字的操作函数和协议。
*/
sock->ops = answer->ops;
//获得tcp_port
answer_prot = answer->prot;
answer_flags = answer->flags;
rcu_read_unlock(); // 解锁 RCU 读锁
/*
* 分配套接字内存。
* 如果分配失败,返回 -ENOBUFS 错误。
*/
err = -ENOBUFS;
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
if (!sk)
goto out;
/*
* 初始化套接字。
*/
err = 0;
if (INET_PROTOSW_REUSE & answer_flags)
sk->sk_reuse = SK_CAN_REUSE; // 设置套接字重用标志
inet = inet_sk(sk); // 获取 IPv4 套接字结构体
inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0; // 设置是否为控制套接字
// 初始化其他 IPv4 特定字段
inet->nodefrag = 0;
if (SOCK_RAW == sock->type) {
inet->inet_num = protocol;
if (IPPROTO_RAW == protocol)
inet->hdrincl = 1;
}
if (net->ipv4.sysctl_ip_no_pmtu_disc)
inet->pmtudisc = IP_PMTUDISC_DONT;
else
inet->pmtudisc = IP_PMTUDISC_WANT;
inet->inet_id = 0;
// 关联套接字和协议
sock_init_data(sock, sk);
sk->sk_destruct = inet_sock_destruct;
sk->sk_protocol = protocol;
sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
inet->uc_ttl = -1;
inet->mc_loop = 1;
inet->mc_ttl = 1;
inet->mc_all = 1;
inet->mc_index = 0;
inet->mc_list = NULL;
inet->rcv_tos = 0;
sk_refcnt_debug_inc(sk); // 增加套接字引用计数
// 如果设置了特定协议号,添加到协议哈希链
if (inet->inet_num) {
err = sk->sk_prot->hash(sk);
if (err) {
sk_common_release(sk);
goto out;
}
}
// 如果协议需要,调用协议的初始化函数
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
if (err) {
sk_common_release(sk);
goto out;
}
}
// 对于非内核进程,运行 BPF cgroup 套接字挂钩程序
if (!kern) {
err = BPF_CGROUP_RUN_PROG_INET_SOCK(sk);
if (err) {
sk_common_release(sk);
goto out;
}
}
out:
return err; // 返回结果
out_rcu_unlock:
rcu_read_unlock(); // 解锁 RCU 读锁
goto out; // 跳转到函数出口
}
在inet_create中根据类型SOCK_STREAM查找到对于TCP定义的操作方法实现集合inet_stream_ops和tcp_port,并把它们分别设置到socket->ops和sock->sk_port上。
void sock_init_data(struct socket *sock, struct sock *sk)
{
/* 初始化套接字通用字段 */
sk_init_common(sk);
...
/* 设置套接字的接收和发送缓冲区大小 */
sk->sk_rcvbuf = sysctl_rmem_default; // 接收缓冲区默认大小
sk->sk_sndbuf = sysctl_wmem_default; // 发送缓冲区默认大小
...
/* 设置套接字状态为 TCP_CLOSE(关闭状态) */
sk->sk_state = TCP_CLOSE;
/* 将套接字与 socket 结构体关联 */
sk_set_socket(sk, sock);
/* 设置套接字的回调函数 */
//当套接字的状态发生变化时,这个回调函数被调用。例如,套接字从监听状态变为已连接状态,或者从已连接状态变为关闭状态。
sk->sk_state_change = sock_def_wakeup;
//当套接字接收队列中有数据可读时,这个回调函数被调用。它通知套接字数据已经准备好,可以被用户空间读取。
sk->sk_data_ready = sock_def_readable;
//当套接字的发送队列有足够的空间来接受新的数据时,这个回调函数被调用。它通知套接字发送缓冲区不再满,可以发送更多数据。
sk->sk_write_space = sock_def_write_space;
//当套接字遇到错误时,这个回调函数被调用。它负责向用户空间报告错误,例如连接重置、数据传输错误等。
sk->sk_error_report = sock_def_error_report;
//当套接字被销毁时,这个回调函数被调用。它负责执行清理操作,释放套接字占用的资源。
sk->sk_destruct = sock_def_destruct;
/* 设置套接字的接收和发送超时时间 */
sk->sk_rcvtimeo = MAX_SCHEDULE_TIMEOUT;
sk->sk_sndtimeo = MAX_SCHEDULE_TIMEOUT;
...
/* 设置套接字的时间戳 */
sk->sk_stamp = SK_DEFAULT_STAMP;
}
EXPORT_SYMBOL(sock_init_data);
2、内核IO和用户进程协作之阻塞方式
同步阻塞IO总体流程如下:
2.1 等待接收消息
recv函数通过strace命令跟踪,可以看到recv会执行recvform调用。
下面从源代码看看recvfrom是怎么把自己阻塞掉的。
SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
unsigned int, flags, struct sockaddr __user *, addr,
int __user *, addr_len)
{
...
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
// 根据用户传入的fd找到socket对象
sock = sockfd_lookup_light(fd, &err, &fput_needed);
...
err = sock_recvmsg(sock, &msg, flags);
...
}
接下来调用顺序为:
sock_recvmsg==>sock_recvmsg_nosec==>inet_recvmsg==>tcp_recvmsg
int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags)
{
int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags);
return err ?: sock_recvmsg_nosec(sock, msg, flags);
}
static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,
int flags)
{
return sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);
}
int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
int flags)
{
struct sock *sk = sock->sk;
int addr_len = 0;
int err;
sock_rps_record_flow(sk);
err = sk->sk_prot->recvmsg(sk, msg, size, flags & MSG_DONTWAIT,
flags & ~MSG_DONTWAIT, &addr_len);
if (err >= 0)
msg->msg_namelen = addr_len;
return err;
}
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
...
do {
...
//遍历接收队列接收数据
skb_queue_walk(&sk->sk_receive_queue, skb) {
...
//数据接收完成则返回
if (copied >= target && !sk->sk_backlog.tail)
break;
if (copied >= target) {
/* Do not sleep, just process backlog. */
release_sock(sk);
lock_sock(sk);
} else {
//没有收到足够的数据,启用sk_wait_data阻塞当前进程,等待数据到来的通知
sk_wait_data(sk, &timeo, last);
}
...
} while (len > 0);
}
EXPORT_SYMBOL(tcp_recvmsg);
这里可以看到,skb_queue_Walk在访问sock对象下的接收队列,如果没有收到数据或者收到的不够多,那么调用sk_wait_data将当前进程阻塞。
int sk_wait_data(struct sock *sk, long *timeo, const struct sk_buff *skb)
{
//当前进程关联到所定义的等待队列项上,并设置唤醒回调函数
DEFINE_WAIT_FUNC(wait, woken_wake_function);
int rc;
//添加等待队列项到sock的等待队列中
add_wait_queue(sk_sleep(sk), &wait);
sk_set_bit(SOCKWQ_ASYNC_WAITDATA, sk);
//通过调用wait_woken让出CPU,然后进入睡眠
rc = sk_wait_event(sk, timeo, skb_peek_tail(&sk->sk_receive_queue) != skb, &wait);
sk_clear_bit(SOCKWQ_ASYNC_WAITDATA, sk);
remove_wait_queue(sk_sleep(sk), &wait);
return rc;
}
#define sk_wait_event(__sk, __timeo, __condition, __wait) \
({ int __rc; \
release_sock(__sk); \
__rc = __condition; \
if (!__rc) { \
*(__timeo) = wait_woken(__wait, \
TASK_INTERRUPTIBLE, \
*(__timeo)); \
} \
sched_annotate_sleep(); \
lock_sock(__sk); \
__rc = __condition; \
__rc; \
})
当内核收完数据产生就绪事件的时候,会通过回调查找socket等待队列项,进而可以找到回调函数和在等待该socket就绪事件的进程。
2.2 软中断模块
网络包从网卡后怎么接收再交给软中断处理的,这篇文章有描述:添加链接描述
下面从tcp_v4_rcv的源码开始看,总体接收流程如下:
软中断里收到数据之后,发现是TCP包就会执行tcp_v4_rcv函数。如果是ESTABLISH状态下的数据包,则最终会把数据解析出来放到对应socket的接收队列中,然后调用sk_data_ready来唤醒用户进程。
int tcp_v4_rcv(struct sk_buff *skb)
{
...
th = (const struct tcphdr *)skb->data; //获取tcp header
iph = ip_hdr(skb); //获取ip header
...
lookup:
//根据数据包header中的IP、端口信息查找到对应的socket
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, &refcounted);
if (!sk)
goto no_tcp_socket;
process:
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
if (sk->sk_state == TCP_NEW_SYN_RECV) {
struct request_sock *req = inet_reqsk(sk);
struct sock *nsk;
sk = req->rsk_listener;
if (unlikely(tcp_v4_inbound_md5_hash(sk, skb))) {
sk_drops_add(sk, skb);
reqsk_put(req);
goto discard_it;
}
if (unlikely(sk->sk_state != TCP_LISTEN)) {
inet_csk_reqsk_queue_drop_and_put(sk, req);
goto lookup;
}
/* We own a reference on the listener, increase it again
* as we might lose it too soon.
*/
sock_hold(sk);
refcounted = true;
nsk = tcp_check_req(sk, skb, req, false);
if (!nsk) {
reqsk_put(req);
goto discard_and_relse;
}
if (nsk == sk) {
reqsk_put(req);
} else if (tcp_child_process(sk, nsk, skb)) {
tcp_v4_send_reset(nsk, skb);
goto discard_and_relse;
} else {
sock_put(sk);
return 0;
}
}
if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
__NET_INC_STATS(net, LINUX_MIB_TCPMINTTLDROP);
goto discard_and_relse;
}
if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
goto discard_and_relse;
if (tcp_v4_inbound_md5_hash(sk, skb))
goto discard_and_relse;
nf_reset(skb);
if (tcp_filter(sk, skb))
goto discard_and_relse;
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
skb->dev = NULL;
if (sk->sk_state == TCP_LISTEN) {
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
//socket未被用户锁定
if (!sock_owned_by_user(sk)) {
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
goto discard_and_relse;
}
}
在tcp_v4_do_rcv中,首先找到对应skb对应的socket,然后进入tcp_v4_do_rcv。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst = sk->sk_rx_dst;
sock_rps_save_rxhash(sk, skb);
sk_mark_napi_id(sk, skb);
if (dst) {
if (inet_sk(sk)->rx_dst_ifindex != skb->skb_iif ||
!dst->ops->check(dst, 0)) {
dst_release(dst);
sk->sk_rx_dst = NULL;
}
}
//执行连接状态下的数据处理
tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len);
return 0;
}
//其他非ESTABLISH状态的数据包处理
...
}
在tcp_v4_do_rcv中会调用tcp_rcv_established,在tcp_rcv_established中会调用tcp_queue_rcv把数据添加到接收队列末尾,然后调用sock的sk_data_ready函数指针,这个指针在sock初始化的时候已经被设置成了sock_def_readable,所以会调用到sock_def_readable。
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
struct tcp_sock *tp = tcp_sk(sk); // 获取 TCP 套接字的特定结构体
skb_mstamp_get(&tp->tcp_mstamp); // 获取当前时间戳
if (unlikely(!sk->sk_rx_dst)) // 检查接收目的地是否已设置
inet_csk(sk)->icsk_af_ops->sk_rx_dst_set(sk, skb); // 设置接收目的地
/*
* 头部预测。
* 代码大致遵循 Van Jacobson 的 "30 instruction TCP receive"。
*/
tp->rx_opt.saw_tstamp = 0; // 初始化接收选项
/* 检查 TCP 头部的有效性 */
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
int tcp_header_len = tp->tcp_header_len;
if (len <= tcp_header_len) {
...
if (!eaten) {
...
/* 大量数据传输:接收方 */
eaten = tcp_queue_rcv(sk, skb, tcp_header_len, &fragstolen); // 将数据包排入接收队列
}
tcp_event_data_recv(sk, skb); // 触发数据接收事件
if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
/* 处理 ACK */
tcp_ack(sk, skb, FLAG_DATA);
tcp_data_snd_check(sk); // 检查发送数据
if (!inet_csk_ack_scheduled(sk))
goto no_ack;
}
__tcp_ack_snd_check(sk, 0); // 检查 ACK 发送
no_ack:
if (eaten)
kfree_skb_partial(skb, fragstolen); // 释放部分数据包
sk->sk_data_ready(sk); // 通知套接字有数据可读
return;
}
}
...
}
EXPORT_SYMBOL(tcp_rcv_established);
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen,
bool *fragstolen)
{
int eaten;
struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);
__skb_pull(skb, hdrlen);
eaten = (tail &&
tcp_try_coalesce(sk, tail, skb, fragstolen)) ? 1 : 0;
tcp_rcv_nxt_update(tcp_sk(sk), TCP_SKB_CB(skb)->end_seq);
//将接收到的数据添加到尾部
if (!eaten) {
__skb_queue_tail(&sk->sk_receive_queue, skb);
skb_set_owner_r(skb, sk);
}
return eaten;
}
static void sock_def_readable(struct sock *sk)
{
struct socket_wq *wq;
rcu_read_lock();
wq = rcu_dereference(sk->sk_wq);
if (skwq_has_sleeper(wq))
//唤醒等待队列的进程
wake_up_interruptible_sync_poll(&wq->wait, POLLIN | POLLPRI |
POLLRDNORM | POLLRDBAND);
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
rcu_read_unlock();
}
#define wake_up_interruptible_sync_poll(x, m) \
__wake_up_sync_key((x), TASK_INTERRUPTIBLE, 1, (void *) (m))
//nr_exclusive被宏定义成了1,是为了防止惊群
void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;
int wake_flags = 1; /* XXX WF_SYNC */
if (unlikely(!q))
return;
if (unlikely(nr_exclusive != 1))
wake_flags = 0;
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, wake_flags, key);
spin_unlock_irqrestore(&q->lock, flags);
}
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
//调用进程加入等待队列时注册的回调函数
if (curr->func(curr, mode, wake_flags, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
__wake_up_common实现唤醒,该函数调用的传入参数nr_exclusive写死了为1,这里是指即使有多个进程都阻塞在同一个socket上,也只唤醒一个进程。其作用是为了避免“惊群”。在recv注册等待队列项的时候,内核把curr->func设置成了woken_wake_function;在woken_wake_function中调用了woken_wake_function,最终调用到了try_to_wake_up,调用try_to_wake_up传入的参数curr->private就是当时因为等待而被阻塞的进程任务。当这个函数执行完的时候,在socket上等待而被阻塞的进程就被推入可运行队列了。
int woken_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
wait->flags |= WQ_FLAG_WOKEN;
return default_wake_function(wait, mode, sync, key);
}
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,
void *key)
{
return try_to_wake_up(curr->private, mode, wake_flags);
}
同步阻塞整体流程图如下:
文章内容参考:《深入理解Linux网络》
Linux版本用的是4.12,书中用的是3.10