一、IO
1、IO 类型
大部分的socket接口都是阻塞型的。所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。select系统调用是用来让我们的程序监视多个文件句柄的状态变化的,需要指明select函数与【阻塞/非阻塞socket】没有关系。select函数本身调用是阻塞的(与socket是否阻塞并没有关系), 直到有监测事件发生(返回 > 0)、 超时(返回0)、select函数错误 (返回-1)。一个套接字socket阻塞或者不阻塞,select就在那里,它可以针对这2种套接字使用,对任何一种套接字的轮询检测,超时时间都是有效的,区别就在于:当select完毕,认为该套接字可读时:阻塞的套接字,会让read阻塞,直到读到所需要的所有字节;非阻塞的套接字,会让read读完fd中的数据后就返回,但如果原本你要求读10个数据,这时只读了8个数据,如果你不再次使用select来判断它是否可读,而是直接read,很可能返回EAGAIN或=EWOULDBLOCK(BSD风格) ,此错误由在非阻塞套接字上不能立即完成的操作返回。
2、select
当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep 指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中处于运行状态的进程分为两种情况: 正在被调度执行和就绪状态。假设一个进程同时监视多个设备,如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read 调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处理。在open 一个设备时指定了O_NONBLOCK 标志,read / write 就不会阻塞。以read 为例,如果设备暂时没有数据可读就返回-1,同时置errno 为EWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里,那么调用者不是阻塞在这里死等,这样可以同时监视多个设备。 非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里操作系统可以调度别的进程执行,就不会做无用功了。select 函数可以阻塞地同时监视多个设备,还可以设定阻塞等待的超时时间,从而圆满地解决了这个问题。
epoll:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_create() :创建一个 epoll 对象
epoll_ctl():向这个 epoll 对象(红黑树)中添加、删除、修改要管理的连接。epoll_ctl函数的四个参数分别如下所示,通过这些参数,epoll_ctl函数可以在运行时动态地添加、修改或删除文件描述符的事件,以便在事件发生时通知用户空间程序。
-
epoll实例的文件描述符:这是由epoll_create函数返回的文件描述符,它用于标识一个epoll实例。
-
操作类型:这是epoll_ctl函数的核心参数,它是一个宏,表示要执行的操作。常见的操作类型有EPOLL_CTL_ADD、EPOLL_CTL_MOD和EPOLL_CTL_DEL,分别表示添加新事件、修改已注册的事件和删除事件。
-
文件描述符:这是要监控的文件描述符,它可以是已存在的文件、套接字等。
-
事件:这个参数是一个结构体,用于指定要监控的事件类型。常见的类型包括EPOLLIN(可读事件)、EPOLLOUT(可写事件)和EPOLLERR(错误事件)等。
epoll_wait():等待其管理连接上的 IO 事件
epoll 之所以做到了高效,最关键的两点:
-
内部管理 fd 使用了高效的红黑树结构管理,做到了增删改之后性能的优化和平衡;
-
epoll 池添加 fd 的时候,调用 file_operations->poll ,把这个 fd 就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行;
-
epoll 池核心的两个数据结构:红黑树和就绪列表。红黑树是为了应对用户的增删改需求,就绪列表是 fd 事件就绪之后放置的特殊地点,epoll 池只需要遍历这个就绪链表,就能给用户返回所有已经就绪的 fd 数组;
3、进程IO阻塞睡眠之后怎么醒来进入就绪状态的:
-
阻塞的本质就是将进程的task_struct移出运行队列,添加到等待队列,并且将进程的状态的置为TASK_UNINTERRUPTIBLE或者TASK_INTERRUPTIBLE,重新触发一次 CPU 调度让出 CPU。那线程怎么唤醒呢?线程在加入到等待队列的同时向内核注册了一个