阻塞,非阻塞,同步,异步,I/O多路复用

I/O设备

用户态,内核态(内核缓冲区)

read函数 

阻塞和非阻塞描述的是用户线程调用内核IO操作时的用户线程的状态:阻塞表示在调用内核IO时,用户线程会挂起,直到调用返回到用户空间才继续执行;非阻塞表示在调用内核IO时立即返回一个状态码或者数据,用户线程一直处于忙的状态

阻塞:当我们调用read、send、write等等系统调用API时,会把内核的缓存区里的数据拷贝到用户态的缓存区里,如果这个时候内核缓存区里没有数据,则会等待内核把数据准备好,此时,应用进程处于一种挂起状态。

非阻塞:当我们的应用程序调用write、send、read(所有的IO接口都是的),如果仅仅是从内核缓存区里把数据copy走,或者没有数据copy,此时也会直接返回,明显我们的用户进程并没有任何的等待,或者说处于挂起的状态,这种就是非阻塞。

默认创建的socket是阻塞的,但是可以调用下面这个函数设置为非阻塞

同步/异步

同步和异步描述的是用户线程调用内核IO操作时结果的返回方式,同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

结果的返回方式

同步  通过read返回告诉数据已经准备好了

异步,我调用read时,只是告诉你一个回调函数,并且read已经返回,但是此时并不代表数据准备好了,只有当内核调用了我们的回调函数才意味着数据已经准备好了。

I/O多路复用

如果是阻塞的套接字,服务器在主线程里是没有办法同时处理多个客户端的连接和数据的收发,只有通过多线程或者多进程的方式来做到这个并发。

 如果是单纯的非阻塞的套接字,服务器的CPU会一直处于一种空转的状态。

 这两种编程方式都不能处理多个客户端的并发情况。

在一个线程或者一个进程的情况下处理多个客户端的并发情况(引入多路复用)

I/O多路复用能够使得程序同时监听多个文件描述符

LINUX下select,poll,epoll

 

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

调用select的时候,整个线程会被阻塞起来,

readfds是它文件描述符的容器,管理了很多的socket,那nfds就是其中socket的句柄数最大的那个。

void FD_SET(int fd, fd_set *set);
//添加fd到set里。

void FD_CLR(int fd, fd_set *set);
//从set里删除fd

void FD_ZERO(fd_set *set);
//初始化这个set

int  FD_ISSET(int fd, fd_set *set);
//判断这个fd在set里不是有某个关的事件发生了。
select的不足:
1)select的实现,在内核里它是被定义成一个数组的。数组肯定就有大小,数组的大小被默认定义了1024,默认的情况下只能支持1024个socket的管理。

2)select返回后,应用层需要不断轮询这个fd_set,去判断socket期望的是否发生。

3)select返回后,会把内核管理的这个fd_set清空,依然需要把用户态的socket拷贝到内核的select管理的fd_set,发生频繁从用户态的数据copy到内核里面。

poll的实现,对select进行了改进,就是用链表去保存了socket。也就是说克服了select对socket数量的限制。

epoll

int epfd = epoll_create(int size);
int epfd = epoll_create1(int flags);

返回值也是一个文件句柄,

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

如果op为EPOLL_CTL_ADD,则是把fd添加到epoll里。
如果op为EPOLL_CTL_DEL,则是把fd从epoll里删除。
如果op为EPOLL_CTL_MOD,则是修改fd在epoll里的事件值。

struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };

typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;


int cnt = epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

调用了epoll_wait, 直到epoll管理的socket有相关的事件发生的时候,epoll_wait才会返回。

返回后,所有的事件都存储在events里面,events的size是epoll_wait的返回值。

 

else if (events[i].data.fd == sfd),如果是服务端监听的socket返回了,说明有客户端连接进来了,此时需要调用accpt去接受连接,并且accpt返回-1且errno是EAGAIN或者EWOULDBLOCK则代表需要继续处理,并不是一种异常。

accpt之后,需要调用epoll_ctl的ADD方法把客户端的socket添加到epoll里。

else

    就是客户端发送数据过来了,如果read返回-1且errno为EGAIN或者EWOULDBLOCK则继续处理,不是一种异常,如果read返回0,代表客户端的socket关闭了连接,此时服务器需要close(fd);如果read返回值大于0,则正确地接收到了数据,此时需要process、send。

epoll的设计:

epoll_wait返回的时候,只是把已经收到事件的socket返回(events),没有收到事件的socket并没有返回。克服了select的第二个缺陷。

epoll是怎么设计:

采用了红黑树管理了epoll里的所有的socket句柄,O(lgn),事件发生了,实际上是有文件系统设置的。epoll会把相应的socket添加到一个双向链表中,同时让epoll_wait返回,此时epoll_wait的event指向了这个双向链表。

 

posted @ 2021-09-17 00:02  wsq1219  阅读(108)  评论(0)    收藏  举报