Linux的五种IO模型
本文内容整理至以下资料:
[1] Linux五种IO模型https://blog.csdn.net/z_ryan/article/details/80873449
[2] select() - Unix, Linux System Call https://www.tutorialspoint.com/unix_system_calls/_newselect.htm
[3] Multiplexed I/0 with poll() https://www.linuxtoday.com/blog/multiplexed-i0-with-poll.html
[4] epoll() Tutorial – epoll() In 3 Easy Steps! https://www.suchprogramming.com/epoll-in-3-easy-steps/
[5] signal(SIGIO,XXXX)实例 https://blog.csdn.net/GZFStudy/article/details/51578570
[6] Linux 信号(signal)https://www.jianshu.com/p/f445bfeea40a
在Linux中,对于一次读取IO的操作,数据并不会直接拷贝到程序的程序缓冲区。通常包括两个不同阶段:
(1)等待数据准备好,到达内核缓冲区;
(2)从内核向进程复制数据。
一、阻塞式I/O
进程调用一个recvfrom请求,如果数据没有准备好,则阻塞直到数据返回,然后将数据从内核空间复制到程序空间。
二、非阻塞式I/O
调用非阻塞的recvform系统调用后,进程不会被阻塞,如果数据还没准备好,此时会返回一个error。
进程在返回之后,可以处理其他的业务逻辑,过会儿再发起recvform系统调用。采用轮询的方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。
三、I/O复用(select,poll,epoll等)
特点:IO 多路复用的好处就在于单个进程就可以同时处理多个网络连接的IO
基本原理:不再由应用程序自己监视连接,取而代之由内核替应用程序监视文件描述符。下图来源为参考链接[1]
以select为例,当用户进程调用了select,那么整个进程会被阻塞。
与此同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。
(根据上述描述,select唤醒机制有点像互斥量mutex)
#include <stdio.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int main(void) { fd_set rfds; struct timeval tv; // timeout int retval; /* Watch stdin (fd 0) to see when it has input. */ FD_ZERO(&rfds); FD_SET(0, &rfds); /* Wait up to five seconds. */ tv.tv_sec = 5; tv.tv_usec = 0; retval = select(1, &rfds, NULL, NULL, &tv); /* Don’t rely on the value of tv now! */ if (retval == -1) perror("select()"); else if (retval) printf("Data is available now.\n"); /* FD_ISSET(0, &rfds) will be true. */ else printf("No data within five seconds.\n"); return 0; }
#include <fcntl.h> #include <stdio.h> #include <sys/poll.h> #include <sys/time.h> #include <unistd.h> /* For simplicity, all error checking has been left out */ int main(int argc, char ** argv) { int fd; char buf[1024]; int i; struct pollfd pfds[2]; fd = open(argv[1], O_RDONLY); while (1) { pfds[0].fd = 0; pfds[0].events = POLLIN; pfds[1].fd = fd; pfds[1].events = POLLIN; poll(pfds, 2, -1); if (pfds[0].revents & POLLIN) { i = read(0, buf, 1024); if (!i) { printf("stdin closed\n"); return 0; } write(1, buf, i); } if (pfds[1].revents & POLLIN) { i = read(fd, buf, 1024); if (!i) { printf("file closed\n"); return 0; } write(1, buf, i); } } }
#define MAX_EVENTS 5 #define READ_SIZE 10 #include <stdio.h> // for fprintf() #include <unistd.h> // for close(), read() #include <sys/epoll.h> // for epoll_create1(), epoll_ctl(), struct epoll_event #include <string.h> // for strncmp int main() { int running = 1, event_count, i; size_t bytes_read; char read_buffer[READ_SIZE + 1]; struct epoll_event event, events[MAX_EVENTS]; int epoll_fd = epoll_create1(0); if(epoll_fd == -1) { fprintf(stderr, "Failed to create epoll file descriptor\n"); return 1; } event.events = EPOLLIN; event.data.fd = 0; if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event)) { fprintf(stderr, "Failed to add file descriptor to epoll\n"); close(epoll_fd); return 1; } while(running) { printf("\nPolling for input...\n"); event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, 30000); printf("%d ready events\n", event_count); for(i = 0; i < event_count; i++) { printf("Reading file descriptor '%d' -- ", events[i].data.fd); bytes_read = read(events[i].data.fd, read_buffer, READ_SIZE); printf("%zd bytes read.\n", bytes_read); read_buffer[bytes_read] = '\0'; printf("Read '%s'\n", read_buffer); if(!strncmp(read_buffer, "stop\n", 5)) running = 0; } } if(close(epoll_fd)) { fprintf(stderr, "Failed to close epoll file descriptor\n"); return 1; } return 0; }
四、信号驱动式I/O(SIGIO)
信号驱动IO,进程注册一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。下列代码来源为参考链接[6]
#include<signal.h> #include<stdio.h> #include <unistd.h> //void (*sa_sigaction)(int, siginfo_t *, void *); void handler(int signum, siginfo_t * info, void * context) { if(signum == SIGIO) printf("SIGIO signal: %d\n", signum); else if(signum == SIGUSR1) printf("SIGUSR1 signal: %d\n", signum); else printf("error\n"); if(context) { printf("content: %d\n", info->si_int); printf("content: %d\n", info->si_value.sival_int); } } int main(void) { //int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction act; /* struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; }; */ act.sa_sigaction = handler; act.sa_flags = SA_SIGINFO; sigaction(SIGIO, &act, NULL); sigaction(SIGUSR1, &act, NULL); for(;;) { sleep(10000); } return 0; }
#include <sys/types.h> #include <signal.h> #include<stdio.h> #include <unistd.h> int main(int argc, char** argv) { if(4 != argc) { printf("[Arguments ERROR!]\n"); printf("\tUsage:\n"); printf("\t\t%s <Target_PID> <Signal_Number> <content>\n", argv[0]); return -1; } int pid = atoi(argv[1]); int sig = atoi(argv[2]); if(pid > 0 && sig > 0) { //int sigqueue(pid_t pid, int sig, const union sigval value); union sigval val; val.sival_int = atoi(argv[3]); printf("send: %d\n", atoi(argv[3])); sigqueue(pid, sig, val); } else { printf("Target_PID or Signal_Number MUST bigger than 0!\n"); } return 0; }
五、异步I/O(POSIX的aio_系列函数)
上述四种IO模型都是同步的。相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,就可以去处理其他的逻辑了,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。等到数据准备好了,内核直接复制数据到进程空间,然后从内核向进程发送通知,此时数据已经在用户空间了,可以对数据进行处理了。
"Network I/O is not a priority for AIO because everyone writing POSIX network servers uses an event based, non-blocking approach. The old-style Java "billions of blocking threads" approach sucks horribly.
Disk write I/O is already buffered and disk read I/O can be prefetched into buffer using functions like posix_fadvise. That leaves direct, unbuffered disk I/O as the only useful purpose for AIO.
Direct, unbuffered I/O is only really useful for transactional databases, and those tend to write their own threads or processes to manage their disk I/O.
So, at the end that leaves POSIX AIO in the position of not serving any useful purpose. Don't use it." From https://stackoverflow.com/questions/87892/what-is-the-status-of-posix-asynchronous-i-o-aio
六、总结
以read()为例,
阻塞I/O:如果缓冲区里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据;
非阻塞I/O:如果缓冲区里没有数据,返回表示读取错误;(相较于阻塞I/O,可以while控制可以读取时就读取,其他时间做别的)
I/O复用:如果缓冲区没有数据,select()阻塞,直到收到数据,select()返回,执行其后的read();(相较于非阻塞I/O,不用程序检查是否可读取,由内核完成检查,并且epoll可以同时监控多个文件描述符)
信号驱动式I/O:进程注册一个信号处理函数。当数据准备好时,进程会收到一个SIGIO信号,进入对应函数进行处理。
异步I/O:异步非阻塞跟同步非阻塞的区别在于,即使缓冲区中有数据时,调用aio_read后,程序也会去处理其他逻辑;而同步非阻塞仅仅是缓冲区无数据时不阻塞。
下图来源为参考链接[1]



浙公网安备 33010602011771号