read
read是一种阻塞I/O,调用时,用户进程会在内核态查看socket的接收缓冲区是否有数据
- 有数据,则拷贝回用户空间,然后返回
- 无数据,进程让出
cpu变成阻塞,当数据到达缓冲区唤醒进程,变成就绪状态等待调度
这里值得关注的是用户进程是如何被唤醒的。
- 当调用
read时,用户进程从用户态变为内核态 - 在
struct task_struct结构找到fd_array,然后根据Socket文件描述符fd找到对应的struct file,调用对应的file_operation,即socket_read_iter - 找到对应的
struct socket,然后调用socket->ops->recvmsg,这里调用的是inet_stream_ops集合中的inet_recvmsg inet_recvmsg会找到struct sock,调用sock->skprot->recvmsg,这里调用的是tcp_prot集合中定义的tcp_recvmsg
阻塞IO中用户进程阻塞以及唤醒原理
接下来重点是tcp_recvmsg是如何阻塞用户进程的
- 调用
sk_sleep(sk)获取struct sock队列中的等待队列头指针wait_queue_head_t - 调用
prepare_to_wait将新创建的等待项wait_queue_t插入等待队列,并将进程设置为INTERRUPTIBLE - 调用
sk_wait_event让出CPU,进程进入睡眠状态
这样进程进入了睡眠状态,并且放在等待队列上
接下来是进程如何被唤醒
-
当数据到达网卡的时候,网卡通过
DMA的方法将数据放到RingBuffer中 -
向
cpu发起硬中断,在硬中断过程中创建sk_buffer,将数据拷贝到sk_buffer中 -
然后发起软中断,内核线程
ksoftirqd响应软中断,调用poll函数将sk_buffer送往内核协议栈进行处理 -
在传输层
tcp_rcv函数中,去掉TCP头,根据四元组(源IP,源端口,目的IP,目的端口)查找对应的socket -
将
sk_buffer放入socket的接收队列里 -
放到接收队列上后,会调用
sk_data_ready,这个函数指针指向了sock_def_readable -
sock_def_readable会去等待队列,通过wake_up_common函数从等待队列里拿出一个wait_queue_t,调用注册的回调函数autoremove_wake_function,根据wait_queue_t上关联的fd,唤醒阻塞在该fd对应的socket上的进程- 注意为了避免惊群效应,这里只会唤醒一个
socket
- 注意为了避免惊群效应,这里只会唤醒一个
epoll
epoll主要有3个api
epoll_create:创建一个epoll实例,返回一个文件描述符fdepoll_ctl:向创建的epoll实例里添加一个fd,监听epoll_wait:阻塞等待,如果有新的事件发生则返回新的事件
epoll_create
epoll_create是操作系统提供的一个系统调用。当调用epoll_create时,系统会创建一个struct eventpoll对象,并且有相关的struct file,file的fd也会被加入当前文件列表,且file_operations的指针指向的是eventpoll_fops操作函数集合。
eventepoll
struct eventepoll {
// 等待队列,阻塞在epoll上的进程会放在这里
wait_queue_head_t wq;
// 就绪队列,I/O就绪的socket连接会放在这里
struct list head rdlist;
// 红黑树管理所有socket连接
struct rb_root rbr;
}wait_queue_head_t:存放的是epoll上所有阻塞的,等待就绪I/O的用户socket,当有I/O就绪的时候,可以通过这个队列寻找阻塞的进程并唤醒,然后获取就绪I/O,到对应socket上去读写数据struct list head rdlist:存放的是就绪的socket I/O,唤醒的用户进程直接来这里获得对应的socket,无需遍历整个集合struct rb_root rbr:红黑树,高效插入删除所有socket连接,相比select和poll使用链表或数组性能更好
epoll_ctl
用于向epoll添加监听的socket,这里在epoll内核创建一个表示Socket连接的数据结构struct epitem,为了性能考虑,这里使用红黑树维护海量的socket连接。
先在socket等待队列中创建好等待项wait_queue_t,设置好回调函数ep_poll_callback,然后通过epoll_entry关联epitem,最后将epitem插入红黑树。
这里一个优化点是,添加socket连接的时候不用像select和poll一样每次全量传入内核所有socket连接,而只用通过红黑树维护增量即可。
epoll_wait
用于阻塞等待事件发生,一次获取多个epoll_event。
- 首先查看
eventpoll->rdlist是否有I/O就绪的eptiem,有的话,通过eptiem找到对应的socket,返回。 - 如果
eventpoll->rdlist里没有I/O就绪的eptiem,就创建一个等待项wait_queue_t,然后将用户进程fd关联到wait_queue_t->private上,并在wait_queue_t->func上注册回调函数default_wake_function,最后添加到epoll的等待队列上,阻塞让出CPU。
唤醒流程
- 首先是
socket传来了数据,网络数据包在经过软中断、内核协议栈处理后到达socket的接收缓冲区,接着调用socket数据就绪回调指针sk_data_ready,回调函数为sock_def_readable。在socket等待队列中找到等待项,其中等待项的回调函数为ep_poll_callback。 - 在回调函数
ep_poll_callback中,根据struct epoll_entry中的struct wait_queue_t wait通过contain_of宏找到epoll_entry对象,并通过base指针找到对应的红黑树节点eptiem,将这个eptiem加入到rdlist里 - 然后查看
epoll中是否有等待进程,如果没有则软中断完成,否则调用回调函数default_wake_function,在回调函数中唤醒阻塞的进程,并将rdlist里的eptiem封装成epoll_event返回 - 以上获取了
I/O就绪的socket,然后读取里面的数据
水平/边缘触发
水平和边缘触发的区别在于,当一次epoll_wait获取了返回的epoll_envet后,是否清空rdlist。
- 水平触发:当一次
epoll_wait获取到I/O就绪的socket后,对socket进行系统调用读取数据。假设单次读取没有全部读取完socket缓冲区的数据,epoll_wait会检查socket,如果没有读完,会将socket放回rdlist,这样下次读的时候不需要阻塞可以直接读取 - 边缘触发:直接清空
rdlist,不管socket中是否还有数据。这样如果想再次读取这个socket,只能等socket再次获取网络数据包,注册rdlist