如果使用多进程/多线程模式的话,创建进程和创建线程需要时间开销。在编写服务器客户端程序时,如果服务器性能不行而 客户端太多时这种代价很大。试想如果有一种方法能够同时监听按键设备、串口设备和网络socket的事件(可读、可写、出错),一旦事件发生就通知大家,并告诉是谁的事件、以及究竟什么事件发生了那就好了,这种实现方式就叫做多路复用。
1.2什么是IOI/O在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。
I/O按照设备来分的话,分为两种,其一是网络I/O,也就是通过网络进行数据的拉取和输出。还有一种是磁盘I/O,主要是对磁盘进行读写工作。
例如当我们打开浏览器进入百度页面查询资料,这个动作里就有Input和Output两个操作,首先浏览器需要发送数据到百度服务器告诉它此时你需要浏览百度我想要的html,这是output,而百度接收数据后返回我需要的数据给浏览器时此时变为input。而当我们仅向内存输入数据,便只需input,若仅从磁盘读数据便只需output。
2.同步异步、阻赛非阻塞在Linux下进行网络编程时,我们常常见到同步(Sync)/异步(Async),阻塞(Block)/非阻塞(Unblock)四种调用方式:
2.1·同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
打个比方,朋友让你帮忙送东西,若朋友采用同步请求的机制通知你,那此时的你在收到朋友的消息后并不会立马回复朋友消息,而是选择送到后,再回复朋友。若朋友采用异步请求的机制通知你,那当你收到消息时,你便会回复朋友,“好的!”再去送,等送到后再通过回调等方式通知朋友。
所以,同步和异步最大的区别就是被调用方的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。
2.2·阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作在没有接收完数据或者没有得到结果之前不会返回,需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
还是前一个例子,若采用阻塞方式,朋友在通知你后会一直等待你,别的什么事也不干。而若采用非阻塞的方式,朋友再告知你后便会去干自己的事,而不用一直等待你的消息。
2.3同步异步与阻塞非阻塞区别
同步异步、阻塞非阻塞的描述对象并不相同,两者互不冲突。
阻塞与非阻塞是形容调用者的,而同步和异步是形容被调用者完成被调用的工作这件事。
同步里面可以有阻塞的情形,也可以有非阻塞的性情。异步也可以有阻塞的情形和非阻塞的情形,两者并不矛盾。
IO复用包括 select, poll, epoll 三种模式,那么什么是IO多路复用,这三种模式又有什么区别呢?
1.IO 多路复用概念:I/O 多路复用技术是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O 系统调用。
总结来说,IO多路复用, 是找一个代抢课来帮你蹲守选课系统进行抢课, 这个期间你可以做些其他的事情,例如可以顺便玩玩游戏、看看电视剧等等。
2.三种模式的区别(以下概念介绍以举例方式说明)
2.1select代选: 每有一个课程容量, select代选都不知道这个是不是你的需要的课程, 她需要一个一个询问, 并且select待选能力还有限, 最多一次帮你监视1024个课程容量;
2.2poll代选: 不限制盯着课程容量的数量, 只要是有容量的课程可选, 都会帮你去问是不是你需要的课程;
2.3epoll代选 :不限制盯着课程的数量, 并且也不需要一个一个去问. 那么如何做呢? epoll待选会为每个进系统里课程贴上一个大字条,上面写上课程的名字, 只要课程空余, epoll代选就知道这个是不是你所需要的课程, 然后代选再通知你;
3.三种模式流程图 3.1selectselect()函数允许进程指示内核等待多个事件(文件描述符)中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时 间后才唤醒它,然后接下来判断究竟是哪个文件描述符发生了事件并进行相应的处理。
select的缺点:
1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫 描文件描述符,文件描述符数量越多,性能越差;
2. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调 用还是会将这些文件描述符通知进程。
使用select()多路复用实现的服务器端示例代码
#include3.2poll#include #include #include #include #include #include #include #include #include #include #include #include #include #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) static inline void msleep(unsigned long ms); static inline void print_usage(char *progname); int socket_server_init(char *listen_ip, int listen_port); int main(int argc, char **argv) { int listenfd, connfd; int serv_port = 0; int daemon_run = 0; char *progname = NULL; int opt; fd_set rdset; int rv; int i, j; int found; int maxfd=0; char buf[1024]; int fds_array[1024]; struct option long_options[] = { {"daemon", no_argument, NULL, 'b'}, {"port", required_argument, NULL, 'p'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} }; progname = basename(argv[0]); while ((opt = getopt_long(argc, argv, "bp:h", long_options, NULL)) != -1) { switch (opt) { case 'b': daemon_run=1; break; case 'p': serv_port = atoi(optarg); break; case 'h': print_usage(progname); return EXIT_SUCCESS; default: break; } } if( !serv_port ) { print_usage(progname); return -1; } if( (listenfd=socket_server_init(NULL, serv_port)) < 0 ) { printf("ERROR: %s server listen on port %d failuren", argv[0],serv_port); return -2; } printf("%s server start to listen on port %dn", argv[0],serv_port); if( daemon_run ) { daemon(0, 0); } for(i=0; imaxfd ? fds_array[i] : maxfd; FD_SET(fds_array[i], &rdset); } rv = select(maxfd+1, &rdset, NULL, NULL, NULL); if(rv < 0) { printf("select failure: %sn", strerror(errno)); break; } else if(rv == 0) { printf("select get timeoutn"); continue; } if ( FD_ISSET(listenfd, &rdset) ) { if( (connfd=accept(listenfd, (struct sockaddr *)NULL, NULL)) < 0) { printf("accept new client failure: %sn", strerror(errno)); continue; } found = 0; for(i=0; i
poll()的机制与 select() 类 似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
poll函数原型
#includestruct pollfd { int fd; short events; short revents; } ; int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd *fd:truct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符。
nfds_t nfds:指定数组中监听的元素个数.
int timeout:指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll() 一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。 这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
下面是使用poll()多路复用实现的服务器端示例代码:
#include3.3epoll#include #include #include #include #include #include #include #include #include #include #include #include #include #include #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) static inline void print_usage(char *progname); int socket_server_init(char *listen_ip, int listen_port); int main(int argc, char **argv) { int listenfd, connfd; int serv_port = 0; int daemon_run = 0; char *progname = NULL; int opt; int rv; int i, j; int found; int max; char buf[1024]; struct pollfd fds_array[1024]; struct option long_options[] = { {"daemon", no_argument, NULL, 'b'}, {"port", required_argument, NULL, 'p'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} }; progname = basename(argv[0]); while ((opt = getopt_long(argc, argv, "bp:h", long_options, NULL)) != -1) { switch (opt) { case 'b': daemon_run=1; break; case 'p': serv_port = atoi(optarg); break; case 'h': print_usage(progname); return EXIT_SUCCESS; default: break; } } if( !serv_port ) { print_usage(progname); return -1; } if( (listenfd=socket_server_init(NULL, serv_port)) < 0 ) { printf("ERROR: %s server listen on port %d failuren", argv[0],serv_port); return -2; } printf("%s server start to listen on port %dn", argv[0],serv_port); if( daemon_run ) { daemon(0, 0); } for(i=0; imax ? i : max; if (--rv <= 0) continue; } else { for(i=1; i
epoll通过在Linux内核中申请一个简易的文件系统,把原先的select/poll调用分成了3 个部分:
1. 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
函数原型:
#includeint epoll_create(int size);
2. 调用epoll_ctl(修改epoll兴趣列表)向epoll对象中添加这100万个连接的套接字
函数原型
#includeint epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
3. 调用epoll_wait收集发生的事件的连接
函数原型
#includeint epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
下面是使用epoll()多路复用实现的服务器端示例代码
#include#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_EVENTS 512 #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) static inline void print_usage(char *progname); int socket_server_init(char *listen_ip, int listen_port); void set_socket_rlimit(void); int main(int argc, char **argv) { int listenfd, connfd; int serv_port = 0; int daemon_run = 0; char *progname = NULL; int opt; int rv; int i, j; int found; char buf[1024]; int epollfd; struct epoll_event event; struct epoll_event event_array[MAX_EVENTS]; int events; struct option long_options[] = { {"daemon", no_argument, NULL, 'b'}, {"port", required_argument, NULL, 'p'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} }; progname = basename(argv[0]); while ((opt = getopt_long(argc, argv, "bp:h", long_options, NULL)) != -1) { switch (opt) { case 'b': daemon_run=1; break; case 'p': serv_port = atoi(optarg); break; case 'h': print_usage(progname); return EXIT_SUCCESS; default: break; } } if( !serv_port ) { print_usage(progname); return -1; } set_socket_rlimit(); if( (listenfd=socket_server_init(NULL, serv_port)) < 0 ) { printf("ERROR: %s server listen on port %d failuren", argv[0],serv_port); return -2; } printf("%s server start to listen on port %dn", argv[0],serv_port); if( daemon_run ) { daemon(0, 0); } if( (epollfd=epoll_create(MAX_EVENTS)) < 0 ) { printf("epoll_create() failure: %sn", strerror(errno)); return -3; } //event.events = EPOLLIN|EPOLLET; event.events = EPOLLIN; event.data.fd = listenfd; if( epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event) < 0) { printf("epoll add listen socket failure: %sn", strerror(errno)); return -4; } for ( ; ; ) { events = epoll_wait(epollfd, event_array, MAX_EVENTS, -1); if(events < 0) { printf("epoll failure: %sn", strerror(errno)); break; } else if(events == 0) { printf("epoll get timeoutn"); continue; } for(i=0; i



