在网络数据帧通过协议栈处理后,内核需要将数据传递给用户空间的进程进行处理。内核与用户进程的协作主要通过两种方式来唤醒用户进程:
同步阻塞(多用于客户端)(Java(BIO))
在这种模式下,用户进程会被阻塞,直到内核有数据可供处理。客户端进程通常使用这种方式进行等待,直到网络数据到达为止。类似于Java中的**BIO(Blocking
I/O)**模式,其中I/O操作会阻塞进程,直到操作完成。
多路I/O复用(多用于服务端)(Java (NIO))
在这种模式下,单个线程监控多个网络连接的状态时,避免每个连接都单独占用一个线程,从而提高资源利用效率。类似于Java中的**NIO(Non-blocking
I/O)**模式,其中I/O操作是非阻塞的,进程可以同时处理多个I/O操作,而不需要等待每个操作完成。
分析内核与用户进程协作相关内容前期准备工作,需要创建并使用 socket
。
同步阻塞I/O模型下,用户进程发起一个I/O请求,必须等待内核将数据准备好并返回给用户进程,在等待期间,用户进程处于阻塞状态,不能执行其他任务,只有当数据准备好并返回给用户进程后,用户进程才可以继续执行。适用于简单的 I/O 操作,例如:本地文件的读写操作。在高并发环境下,同步阻塞 I/O 可能会导致性能问题,因为一个阻塞的 I/O 操作会影响其他操作的执行,内核视角看socket总览图如下:
socket创建主要完成如下工作:
1、分配一个新的 struct socket
对象
2、通过协议族找到合适的协议操作并绑定
3、初始化套接字并设置与用户态进行协作的相关回调函数
#include
#include
#include
#include
#include
#define PORT 8080
int main() {
int sock;
struct sockaddr_in server_addr;
char buffer[1024];
// 创建 socket
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("Socket creation failed");
return 1;
}
// 设置 server 地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 连接到服务器
if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Connection failed");
return 1;
}
printf("Connected to server\n");
// 发送消息给服务器
printf("Enter message: ");
fgets(buffer, sizeof(buffer), stdin);
write(sock, buffer, strlen(buffer));
// 接收服务器响应
int n = read(sock, buffer, sizeof(buffer) - 1);
buffer[n] = '\0';
printf("Server response: %s\n", buffer);
// 关闭 socket
close(sock);
return 0;
}
以socket进行同步I/O操作为例。用户编写程序代码时调用socket
函数创建套接字,write()
会阻塞直到数据能够成功写入缓冲区并发送到对端,read()
会阻塞直到有数据可以读取,用户仅仅看到的是返回的整数句柄sock
,使用该文件描述符进行网络通信。
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
用户在应用程序中调用 socket()
时,操作系统将通过系统调用接口进入内核。内核会执行 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
,并调用内核实现的 __sys_socket()
来处理套接字的创建。
int __sys_socket(int family, int type, int protocol)
{
......
sock = __sys_socket_create(family, type, protocol);
......
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
函数调用关系__sys_socket-->__sys_socket_create-->socket_create-->__socket_create
,__sys_socket()
主要调用 __sys_socket_create
来创建套接字。成功时,它会返回一个文件描述符,这个描述符代表了新创建的套接字,并能够让用户进程通过该文件描述符与内核网络协议栈交互。
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;
sock = sock_alloc();
if (!sock) {
net_warn_ratelimited("socket: no more sockets\n");
return -ENFILE; /* Not exactly a match, but its the
closest posix thing */
}
......
pf = rcu_dereference(net_families[family]);
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;
/*
* We will call the ->create function, that possibly is in a loadable
* module, so we have to bump that loadable module refcnt first.
*/
if (!try_module_get(pf->owner))
goto out_release;
/* Now protected by module ref count */
rcu_read_unlock();
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
.....
}
EXPORT_SYMBOL(__sock_create)
调用sock_alloc
分配struct socket
内核对象,并通过协议族获取对应的 create
操作函数(如 inet_create
),然后调用协议族的 create
方法来初始化套接字并完成具体的协议栈初始化。
//协议族操作结构体
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
根据用户态传入的AF_INET
对应的create函数是inet_create
。
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
struct inet_sock *inet;
struct sock *sk;
struct inet_protosw *answer;
int err;
// 遍历协议栈,找到与指定协议匹配的操作
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
if (protocol == answer->protocol || protocol == IPPROTO_IP) {
// 找到匹配的协议
sock->ops = answer->ops; // 绑定协议操作
break;
}
}
// 分配协议栈的套接字对象
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer->prot, kern);
if (!sk) {
return -ENOMEM;
}
// 初始化套接字对象
sock_init_data(sock, sk);
return 0;
}
遍历套接字类型sock->type
下的所有可用协议栈,inetsw[sock->type]
是一个链表,保存着与套接字类型相关的所有协议,根据传入的protocol
与当前遍历到的协议 answer->protocol
进行协议匹配,匹配成功将匹配到的协议操作函数(answer->ops
)绑定到套接字 sock
上,获取tcp_port
,通过sk_alloc
分配sock对象,将tcp_port
赋于sock->sk_port
。sock_init_data
初始化sock对象。
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, /* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}
};
void sock_init_data(struct socket *sock, struct sock *sk)
{
......
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;//设置套接字销毁时的处理函数
......
}
sock_init_data()
用于初始化套接字的 struct sock
对象,包括设置数据准备、写空间、状态变化等回调函数。当数据准备好或者套接字状态发生变化时,内核会调用相应的回调函数来通知用户进程。数据包经过软中断处理后,会通过 sk->sk_data_ready
指向的函数, sock_def_readable唤醒用户进程,通知其数据已经准备好进行读取。开销:socket系统调用。
同步I/O的核心是通过阻塞进程,等待I/O操作完成后再继续执行,具体完成的内容如下:
触发条件:
当用户程序调用阻塞式I/O函数(如recv()
)时,若内核缓冲区中无数据/数据不足时,进程会进入阻塞状态。
recv() → 系统调用 → tcp_recvmsg_locked → sk_wait_data()
进程状态切换:
内核将进程状态由TASK_RUNNING
(运行态)标记为TASK_INTERRUPTIBLE
(可中断睡眠),并从运行队列移出。
prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
schedule_timeout(timeo); // 让出CPU
等待队列:
每个I/O资源(如Socket)维护一个等待队列,阻塞的进程通过add_wait_queue
加入队列:
DEFINE_WAIT_FUNC(wait, woken_wake_function); // 定义等待项
add_wait_queue(sk_sleep(sk), &wait); // 加入队列
数据路径:
数据包到达网卡后,触发硬件中断,内核通过软中断(如NET_RX_SOFTIRQ
)处理协议栈:
硬中断 → 网卡收包 → 协议栈处理(IP/TCP) → tcp_v4_rcv → tcp_rcv_established
数据入队列:
接收的数据被放入Socket的接收队列:
__skb_queue_tail(&sk->sk_receive_queue, skb); // 数据入队
唤醒阻塞进程:
内核调用sk->sk_data_ready
(如sock_def_readable
),唤醒等待队列中的进程:
wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN); // 唤醒操作
↓
__wake_up_common → default_wake_function → try_to_wake_up()
接收网络数据包使用recv
函数,recv()
默认是阻塞的,意味着如果套接字中没有数据,调用 recv()
会一直阻塞,直到有数据可读取或连接被关闭。可以使用 O_NONBLOCK
标志或 MSG_DONTWAIT
标志让它变成非阻塞行为。
bytes_received = recv(new_sock, buffer, sizeof(buffer) - 1, 0);
同理需要经过系统调用进入内核,调用逻辑为SYSCALL_DEFINE6-->__sys_recvfrom-->sock_recvmsg-->sock_recvmsg_nosec-->inet_recvmsg-->tcp_recvmsg-->tcp_recvmsg_locked
。
static int tcp_recvmsg_locked(struct sock *sk, struct msghdr *msg, size_t len,
int flags, struct scm_timestamping_internal *tss,
int *cmsg_flags)
{
struct tcp_sock *tp = tcp_sk(sk);
int copied = 0;
u32 peek_seq;
u32 *seq;
unsigned long used;
int err;
int target; /* Read at least this many bytes */
long timeo;
struct sk_buff *skb, *last;
u32 urg_hole = 0;
//接收队列数据包
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
}
......
//已读取足够的数据,处理 backlog
if (copied >= target) {
__sk_flush_backlog(sk);
} else {
//无数据可读
tcp_cleanup_rbuf(sk, copied);//清理
sk_wait_data(sk, &timeo, last);//阻塞当前进程
}
}
内核通过skb_queue_walk
遍历接收队列,如果无数据可以读取/可读取数据较少,调用sk_wait_data
阻塞当前进程,如何阻塞?
int sk_wait_data(struct sock *sk, long *timeo, const struct sk_buff *skb)
{
// 定义一个等待结构 `wait`,该结构在满足条件时被唤醒
DEFINE_WAIT_FUNC(wait, woken_wake_function);
int rc;
// 将当前任务添加到套接字的等待队列中
add_wait_queue(sk_sleep(sk), &wait);
// 设置当前套接字的等待状态标志
sk_set_bit(SOCKWQ_ASYNC_WAITDATA, sk);
// 调用 `sk_wait_event` 等待事件发生,直到有数据可读取或者超时
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 DEFINE_WAIT_FUNC(name, function) \
struct wait_queue_entry name = { \
.private = current, \
.func = function, \
.entry = LIST_HEAD_INIT((name).entry), \
}
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
调用DEFINE_WAIT_FUNC
定义一个等待队列项 wait
, private
字段保存了当前进程的描述符,且唤醒时会调用 woken_wake_function
。
static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
return &rcu_dereference_raw(sk->sk_wq)->wait;
}
sk_sleep
返回与套接字 sk
相关联的等待队列头(wait_queue_head_t
类型),进程状态被设置为TASK_INTERRUPTIBLE
,调用add_wait_queue
将等待队列项 wait
插入到 sock
对象的等待队列中。最终调用sk_wait_event
让出CPU,使进程进入阻塞状态,开销:进程上下文切换。
void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;
wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&wq_head->lock, flags);
__add_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
上面步骤是进程被阻塞的过程,那么进程是如何被唤醒呢?
数据包到达网卡时发起硬中断,硬中断处理完成后进行软中断处理,因为传输的是TCP包所有经过软中断处理后交付于协议栈调用的是tcp_v4_rcv
,根据数据包的头部信息中携带的四元组信息找到对应的socket,找到后进入tcp_v4_do_rcv
处理。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
struct dst_entry *dst;
......
tcp_rcv_established(sk, skb);
return 0;
}
//非TCP_ESTABLISHED状态
}
数据包状态TCP_ESTABLISHED
,表示已经建立了连接并且可以传输数据,调用tcp_rcv_established
处理。
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
....
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
tcp_data_ready(sk);
.....
}
调用tcp_queue_rcv
将接收到的数据包放在TCP的接收队列尾部中。
static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb,
bool *fragstolen)
{
int eaten;
struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);
......
if (!eaten) {
__skb_queue_tail(&sk->sk_receive_queue, skb);
skb_set_owner_r(skb, sk);
}
return eaten;
}
当数据准备好后调用tcp_data_ready
,指针sk_data_ready
指向的回调函数sock_def_readable
(sock_init_data
已分析)唤醒用户进程。
void tcp_data_ready(struct sock *sk)
{
if (tcp_epollin_ready(sk, sk->sk_rcvlowat) || sock_flag(sk, SOCK_DONE))
sk->sk_data_ready(sk);
}
void sock_def_readable(struct sock *sk)
{
struct socket_wq *wq;
rcu_read_lock();
//获取套接字的接收队列
wq = rcu_dereference(sk->sk_wq);
//检查是否有进程在socket的等待队列
if (skwq_has_sleeper(wq))
//唤醒等待队列的进程
wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN | EPOLLPRI |
EPOLLRDNORM | EPOLLRDBAND);
//异步唤醒
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
rcu_read_unlock();
}
调用wake_up_interruptible_sync_poll
唤醒此socket上由于等待数据而被阻塞的进程,函数调用逻辑wake_up_interruptible_sync_poll-->__wake_up_sync_key-->__wake_up_common_lock-->__wake_up_common
。
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key,
wait_queue_entry_t *bookmark)
{
......
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
if (flags & WQ_FLAG_BOOKMARK)
continue;
ret = curr->func(curr, mode, wake_flags, key);
if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
......
}
负责从指定的等待队列中唤醒一个或多个任务,nr_exclusive
为1表示多个进程阻塞在同一个socket,也只唤醒1个进程,并非进程全部唤醒。list_for_each_entry_safe_from
遍历等待队列的进程,调用curr->func
唤醒函数,上文分析在DEFINE_WAIT_FUNC
宏中设置了回调函数autoremove_wake_function
。函数调用逻辑autoremove_wake_function-->default_wake_function
。
int default_wake_function(wait_queue_entry_t *curr, unsigned mode, int wake_flags,
void *key)
{
WARN_ON_ONCE(IS_ENABLED(CONFIG_SCHED_DEBUG) && wake_flags & ~WF_SYNC);
return try_to_wake_up(curr->private, mode, wake_flags);
}
调用try_to_wake_up
尝试唤醒等待队列的进程,传入curr->private
也就是由于等待而被阻塞的进程。开销:进程上下文切换(socket等待而被阻塞的进程进入运行队列)。
同步I/O阻塞模型比较简单socket和进程是一对一存在的,针对高并发环境单台机器上承载成千上万个socket连接,那么同步I/O模型明显存在弊端: