目录
一、IO的概念
IO是input(输入)和output(输出)的缩写。计算机作为信息处理工具,首先要通过输入设备接收外界的输入,然后经过cpu的计算,最后通过输出设备把结果输出。因此,io设备是计算机体系结构中不可或缺的组成部分。常用的输入设备有:键盘、鼠标、扫描仪、光笔、摄像头、磁盘等;常用的输出设备有:显示屏、打印机、磁盘等。
二、IO过程
在linux系统下,应用程序通过系统调用 完成io。系统调用是内核提供给用户程序的接口,用户程序通过系统调用完成特定的工作,而不必关系硬件的细节。常用的系统调用如fork()创建进程,open()打开文件,read()、write()读写文件。系统调用的主要作用有:
⑴为用户空间提供了一种硬件的抽象接口
(2)保证了系统的安全和稳定
(3)为进程的运行提供虚拟化的环境
应用程序调用标准io库函数printf() 的完整流程
三、linux下五大IO模型
- 阻塞式io模型(blocking I/O)
-
非阻塞I/O模型
-
IO复用模型
- 信号驱动式I/O模型
-
异步I/O模型
1、阻塞IO
应用进程通过系统调用读取(或写入)数据,当数据还未准备好时,进程会进入睡眠,即阻塞在系统调用,直到数据准备好,系统调用返回。进程拿到数据后进行后续的处理
2、非阻塞IO
进程调用系统调用时通知内核,当请求的I/O操作需要把进程投入睡眠等待时,不把进程投入睡眠,而是返回一个错误。这样进程可以选择继续调用系统调用进行轮询(polling),或者转而处理其他事情。
3、IO多路复用
io复用可以监听、等待多个描述符
4、信号驱动IO
进程通过sigaction系统调用安装一个信号处理函数,该系统调用立即返回,进程继续工作。当数据报准备好时,内核就为进程产生一个SIGIO信号。我们随后就可以在信号处理函数中调用recvfrom读取数据报。这种模型的优势在于,等待数据报到达期间进程不被阻塞。
5、异步IO模型
异步IO由POSIX规范定义。这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。
其中第1到第4种IO模型属于同步IO,因为调用read从内核复制数据到用户空间时,进程时阻塞的;只有第5种IO模型属于异步IO
四、IO多路复用原理
内核提供的IO多路复用机制主要有select、poll、epoll。
1、select系统调用
函数原型:
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *excepset, const struct timeval *timeout);
其中:maxfdp1参数(通常不能超过1024,要设置比1024更大的值需要修改内核代码然后重新编译内核)指定待测试的描述符个数,它的值是待测试的最大描述符加1。
fd_set 是文件描述符集,readset是值-结果参数,传递关心的描述符,函数返回时包含关心的描述符IO事件。指明关系的文件描述符。
timeout参数指明等待时长。
函数返回跨所有描述符集的已就绪的总位数,如果是定时器到时,返回0,返回-1表示出错。
内核提供了三个辅助宏:
void FD_ZERO(fd_set *fdset); //比特位置为0
void FD_SET(int fd, fd_set *fdset); //往描述符集添加文件描述符
void FD_CLR(int fd, fd_set *fdset); //清除描述符
void FD_ISSET(int fd, fd_set *fdset); //判断某个文件描述符的位是否打开
select工作原理:for循环遍历所有关心的描述符,检查每个描述符上是否有IO事件。因此当描述符很多时且事件很少时,需要遍历才能返回,效率直线下降。
2、epoll
epoll包含三个函数:
1、创建epoll句柄
int epoll_create(int size);
成功返回一个epoll句柄,参数size告诉内核监听的数目一共多大。(size参数在内核版本大于2.6.8之后已被弃用)
2、设置事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
➊op参数的值有以下几种:
EPOLL_CTL_ADD 在epfd中注册指定的fd文件描述符并能把event和fd关联起来。
EPOLL_CTL_MOD 改变 fd和event之间的联系。
EPOLL_CTL_DEL 从指定的epfd中删除fd文件描述符。在这种模式中event是被忽略的, 并且为可以等于NULL。
➋fd参数使我们要监听的描述符
➌event这个参数是用于关联制定的fd文件描述符的。它的定义如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
在结构体epoll_event中:
events这个字段是一个字节的掩码构成的。下面是可以用的事件:
EPOLLIN - 当关联的文件可以执行 read ()操作时。
EPOLLOUT - 当关联的文件可以执行 write ()操作时。
EPOLLRDHUP - (从 linux 2.6.17 开始)当socket关闭的时候,或者半关闭写段 的(当使用边缘触发的时候,这个标识在写一些测试代码去检测关 闭的时候特别好用)
EPOLLPRI - 当 read ()能够读取紧急数据的时候。
EPOLLERR - 当关联的文件发生错误的时候,epoll_wait() 总是会等待这个事 件,并不是需要必须设置的标识。
EPOLLHUP - 当指定的文件描述符被挂起的时候。epoll_wait() 总是会等待这 个事件,并不是需要必须设置的标识。当socket从某一个地方读取 数据的时候(管道或者socket),这个事件只是标识出这个已经读取到 最后了(EOF)。所有的有效数据已经被读取完毕了,之后任何的读 取都会返回0(EOF)。
EPOLLET - 设置指定的文件描述符模式为边缘触发,默认的模式是水平触发。
EPOLLONESHOT - (从 linux 2.6.17 开始)设置指定文件描述符为单次模式。这 意味着,在设置后只会有一次从epoll_wait() 中捕获到事件,之后 你必须要重新调用 epoll_ctl() 重新设置。
3、等待事件
int epoll_wait(int epfd, struct epoll_event * events,
int maxevents, int timeout);
①epoll_wait 这个系统调用是用来等待epfd中的事件。
②events指向调用者可以使用的事件的内存区域。
③maxevents告知内核有多少个events,必须要大于0.
④timeout这个参数是用来制定epoll_wait 会阻塞多少毫秒。当timeout等于-1的时候这个函数会无限期的阻塞下去,当timeout等于0的时候,就算没有任何事件,也会立刻返回。
返回值:有多少个IO事件已经准备就绪。如果返回0说明没有IO事件就绪,而是timeout超时。遇到错误的时候,会返回-1,并设置 errno。
示例代码:
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */
epollfd = epoll_create1(0);
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock, (struct sockaddr *) &addr, &addrlen);
if (conn_sock == -1) {perror("accept"); exit(EXIT_FAILURE);}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}
epoll原理:
1、红黑树管理文件描述符。实现快速的查找、删除、更新等操作;
2、双向链表存放就绪的描述符。epoll_waite调用查看链表是否为空即可知道是否有事件产生。
3、基于事件。发生IO事件,回调函数会把就绪的描述符添加到就绪链表,并且唤醒阻塞在epoll_waite系统调用的进程,因此,即使同时监听几万个描述符,epoll的效率也不会下降。
4、slab高速缓存。特定结构体的内存缓存,减少内存频繁分配、回收的开销。
参考资料
《unix环境高级编程》
《unix网络编程》
《Linux 内核设计与实现》