非阻塞IO
当我们调用套接字的读写方法时默认它们是阻塞的,比如read方法要产地进去一个参数n,代表最多读取n个字节后再返回,否则线程就会阻塞在哪里,直到新的数据到来或者连接关闭,read方法才可以返回,线程才能继续处理。write方法不会阻塞,除非内核为socket分配的写缓冲已经满了,直到缓冲区有空闲空间。
非阻塞IO在socket对象上提供了一个选项non_blocking,当这个选项打开时,读写方法不会阻塞,能读多少读多少,能写多少写多少,这个量的大小取决于内核为套接字分配的读缓冲区大小和写缓冲区的空闲大小。read和write方法都会通过返回值告知程序实际读写了多少字节。
事件轮询多路复用
非阻塞IO存在一个问题,那就是线程读数据何时才继续读没读完的数据,也可以理解为当新数据到来时,线程如何得到通知?写数据时如果缓冲区满了没写完,剩下的数据何时才能继续写?
解决这个问题依赖事件轮询API,最简单事件轮询api是select函数,它是操作系统提供给用户程序的api。输入是读写描述符列表,输出是与之对应的可读可写事件。同时还提供一个timeout参数,如果没有任何事件到来,那么就最多等待timeout规定的时间,一旦期间有任何事件到来,就可以立即返回结果,时间过了还没有任何事件到来,也会立即返回。每循环一轮描述符列表称为一个事件循环周期。
因为我们用select同时处理多个通道描述符的读写事件,所以我们将select这类系统调用称为多路复用api。包括redis在内的应用多用epoll(epoll_wait),它们之间主要有如下区别:
- select允许的同时打开的文件描述符远小于epoll
- select每个循环周期会遍历所有文件描述符,而epoll额外维护一个活跃的事件队列,里面只存储了触发事件的文件描述符,所以相比select,在并发比较高时,epoll占有优势
- epoll采用了零拷贝技术,使用mmap系统函数将内核缓冲区与应用程序共享,减少了read和write系统调用,从而减少了内核态和用户态切换
epoll使用例子(来源百度百科)
int epfd = epoll_create(POLL_SIZE);
struct epoll_event ev;
struct epoll_event *events = NULL;
nfds = epoll_wait(epfd, events, 20, 500);
{
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listener) {
//如果是主socket的事件的话,则表示
//有新连接进入了,进行新连接的处理。
client = accept(listener, (structsockaddr *)&local, &addrlen);
if (client < 0) {
perror("accept");
continue;
}
setnonblocking(client); //将新连接置于非阻塞模式
ev.events = EPOLLIN | EPOLLET; //并且将新连接也加入EPOLL的监听队列。
//注意,这里的参数EPOLLIN|EPOLLET并没有设置对写socket的监听,
//如果有写操作的话,这个时候epoll是不会返回事件的,如果要对写操作
//也监听的话,应该是EPOLLIN|EPOLLOUT|EPOLLET
ev.data.fd = client;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev) < 0) {
//设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,
//这里用EPOLL_CTL_ADD来加一个新的epoll事件,通过EPOLL_CTL_DEL来减少一个
//epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。
fprintf(stderr, "epollsetinsertionerror:fd=%d", client);
return -1;
}
}
else if(event[n].events & EPOLLIN)
{
//如果是已经连接的用户,并且收到数据,
//那么进行读入
int sockfd_r;
if ((sockfd_r = event[n].data.fd) < 0)
continue;
read(sockfd_r, buffer, MAXSIZE);
//修改sockfd_r上要处理的事件为EPOLLOUT
ev.data.fd = sockfd_r;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
}
else if(event[n].events & EPOLLOUT)
{
//如果有数据发送
int sockfd_w = events[n].data.fd;
write(sockfd_w, buffer, sizeof(buffer));
//修改sockfd_w上要处理的事件为EPOLLIN
ev.data.fd = sockfd_w;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_w, &ev)
}
do_use_fd(events[n].data.fd);
}
}