目录
1.IO多路复用
1.1基础概念
1.1.1用户空间和内核空间
1.1.2 进程切换
1.1.3 进程阻塞
1.1.4 文件描述符
1.1.5 缓存I/O
1.2 IO多路复用
1.2.1 同步阻塞(BIO)
1.2.2 异步阻塞(NIO)
1.2.3 IO多路复用的三种体现:select,poll,epoll
1.2.3.1 select
1.2.3.3 poll
1.2.3.4 epoll
nginx/redis 所使用的IO模型是什么?
Nginx的IO模型
1、select
2、poll
3、epoll
4、kqueue
5、/dev/poll
6、eventport
Redis IO多路复用技术
epoll 水平触发(LT)与 边缘触发(ET)的区别?
io多路复用的原理和实现_彻底理解 IO 多路复用实现机制_weixin_39934085的博客-CSDN博客
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的,并且进程切换是非常耗费资源的。从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得了CPU资源),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
文件描述符(File descriptor)是一个用于表述指向文件的引用的抽象化概念。 文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。一些涉及底层的程序编写往往会围绕着文件描述符展开。文件描述符往往只适用于UNIX、Linux这样的操作系统。
缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪就会阻塞应用程序,交出CPU。服务器端采用单线程通过 select/poll/epoll 等系统调用获取 fd 列表,遍历有事件的 fd 进行 accept/recv/send ,使其能支持更多的并发连接请求。
服务端采用单线程,当 accept 一个请求后,在 recv 或 send 调用阻塞时,将无法 accept 其他请求(必须等上一个请求处理 recv 或 send 完 )(无法处理并发);
服务端采用多线程,当 accept 一个请求后,开启线程进行 recv,可以完成并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000个线程真正发生读写实际的线程数不会超过20%,每次accept都开一个线程也是一种资源浪费。
服务器端当 accept 一个请求后,加入 fds 集合,每次轮询一遍 fds 集合 recv (非阻塞)数据,没有数据则立即返回错误,每次轮询所有 fd (包括没有发生读写实际的 fd)会很浪费 CPU。
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
select调用过程
(1)使用copy_from_user从用户空间拷贝fd_set到内核空间
(2)注册回调函数__pollwait
(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
select函数接口
#include <sys/select.h> #include <sys/time.h> #define FD_SETSIZE 1024 #define NFDBITS (8 * sizeof(unsigned long)) #define __FDSET_LONGS (FD_SETSIZE/NFDBITS) // 数据结构 (bitmap) typedef struct { unsigned long fds_bits[__FDSET_LONGS]; } fd_set; // API int select( int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout ) // 返回值就绪描述符的数目 FD_ZERO(int fd, fd_set* fds) // 清空集合 FD_SET(int fd, fd_set* fds) // 将给定的描述符加入集合 FD_ISSET(int fd, fd_set* fds) // 判断指定描述符是否在集合中 FD_CLR(int fd, fd_set* fds) // 将给定的描述符从文件中删除
select使用示例
int main() { /* * 这里进行一些初始化的设置, * 包括socket建立,地址的设置等, */ fd_set read_fs, write_fs; struct timeval timeout; int max = 0; // 用于记录最大的fd,在轮询中时刻更新即可 // 初始化比特位 FD_ZERO(&read_fs); FD_ZERO(&write_fs); int nfds = 0; // 记录就绪的事件,可以减少遍历的次数 while (1) { // 阻塞获取 // 每次需要把fd从用户态拷贝到内核态 nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout); // 每次需要遍历所有fd,判断有无读写事件发生 for (int i = 0; i <= max && nfds; ++i) { if (i == listenfd) { --nfds; // 这里处理accept事件 FD_SET(i, &read_fd);//将客户端socket加入到集合中 } if (FD_ISSET(i, &read_fd)) { --nfds; // 这里处理read事件 } if (FD_ISSET(i, &write_fd)) { --nfds; // 这里处理write事件 } } }
select缺点
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。
poll函数接口
#include <poll.h> // 数据结构 struct pollfd { int fd; // 需要监视的文件描述符 short events; // 需要内核监视的事件 short revents; // 实际发生的事件 }; // API int poll(struct pollfd fds[], nfds_t nfds, int timeout);
poll使用示例
// 先宏定义长度 #define MAX_POLLFD_LEN 4096 int main() { /* * 在这里进行一些初始化的操作, * 比如初始化数据和socket等。 */ int nfds = 0; pollfd fds[MAX_POLLFD_LEN]; memset(fds, 0, sizeof(fds)); fds[0].fd = listenfd; fds[0].events = POLLRDNORM; int max = 0; // 队列的实际长度,是一个随时更新的,也可以自定义其他的 int timeout = 0; int current_size = max; while (1) { // 阻塞获取 // 每次需要把fd从用户态拷贝到内核态 nfds = poll(fds, max+1, timeout); if (fds[0].revents & POLLRDNORM) { // 这里处理accept事件 connfd = accept(listenfd); //将新的描述符添加到读描述符集合中 } // 每次需要遍历所有fd,判断有无读写事件发生 for (int i = 1; i < max; ++i) { if (fds[i].revents & POLLRDNORM) { sockfd = fds[i].fd if ((n = read(sockfd, buf, MAXLINE)) <= 0) { // 这里处理read事件 if (n == 0) { close(sockfd); fds[i].fd = -1; } } else { // 这里处理write事件 } if (--nfds <= 0) { break; } } } }
poll缺点
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有缺点:
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是**事件驱动(每个事件关联上fd)**的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))。
epoll函数接口
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:
#include <sys/epoll.h> // 数据结构 // 每一个epoll对象都有一个独立的eventpoll结构体 // 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件 // epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可 struct eventpoll { /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ struct rb_root rbr; /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ struct list_head rdlist; }; // API int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为红黑树元素个数)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:
struct epitem{ struct rb_node rbn;//红黑树节点 struct list_head rdllink;//双向链表节点 struct epoll_filefd ffd; //事件句柄信息 struct eventpoll *ep; //指向其所属的eventpoll对象 struct epoll_event event; //期待发生的事件类型 }
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。 讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。
epoll使用示例
int main(int argc, char* argv[]) { /* * 在这里进行一些初始化的操作, * 比如初始化数据和socket等。 */ // 内核中创建ep对象 epfd=epoll_create(256); // 需要监听的socket放到ep中 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); while(1) { // 阻塞获取 nfds = epoll_wait(epfd,events,20,0); for(i=0;i<nfds;++i) { if(events[i].data.fd==listenfd) { // 这里处理accept事件 connfd = accept(listenfd); // 接收新连接写到内核对象中 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); } else if (events[i].events&EPOLLIN) { // 这里处理read事件 read(sockfd, BUF, MAXLINE); //读完后准备写 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); } else if(events[i].events&EPOLLOUT) { // 这里处理write事件 write(sockfd, BUF, n); //写完后准备读 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); } } } return 0; }
epoll的优点
epoll缺点
epoll LT 与 ET 模式的区别
epoll 有 EPOLLLT 和 EPOLLET 两种触发模式,LT 是默认的模式,ET 是 “高速” 模式。
epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
select/poll/epoll之间的区别
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。
线程的生命周期
2.1 线程池的概念
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行任务结束后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
2.2 使用线程池的原因
多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过度消耗系统资源,以及过度切换线程的危险,从而可能导致系统资源的崩溃。这时线程池就是最好的选择了。
2.3 线程池的c++实现
线程池的主要组成成分有三个部分:
任务队列(Task Queue)
线程池(Thread Pool)
完成队列(Completed Tasks)
#include<iostream> #include<sys/socket.h> #include<netinet/in.h> #include <strings.h> #include <assert.h> #include <zconf.h> #include <cstring> #define PORT 8099 #define LISTENQ 5 int main(){ int listenfd,connfd; struct sockaddr_in servaddr; listenfd=socket(PF_INET,SOCK_STREAM,0); assert(listenfd!=-1); bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_addr.s_addr=htonl(INADDR_ANY); servaddr.sin_port=htons(PORT); int bind_ok=bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)); assert(bind_ok!=-1); int listen_ok=listen(listenfd,LISTENQ); assert(listenfd!=-1); std::string buffer="hello"; int write_ok; while(1) { connfd = accept(listenfd, NULL, NULL); assert(connfd != -1); write_ok = write(connfd, buffer.c_str(), strlen(buffer.c_str())); if (write_ok < 0) { std::cout << "failed" << std::endl; close(connfd); return 0; } close(connfd); } }
网络编程面试题_十早韦的博客-CSDN博客_网络编程面试题