socket
connect()
通知内核,让内核自动完成TCP三次握手,最后把连接的结果返回给这个函数的返回值。
通常,客户端的connect()函数默认会i一直阻塞,直到三次握手成功或超时失败才返回。
listen()
listen(int sockfd, int backlog)函数是不会阻塞的,其主要做的事,将该sockfd和sockfd对应的连接队列长度告诉linux内核,然后,listen函数结束。
两个队列,当有一个客户端主动connect(),linux内核就自动完成三次握手,完成之前(SYN_RCVD状态)这个连接一直在未完成队列中,完成之后(ESTABLISHED状态)将建立好的连接自动存储到完成队列中,并对队列个数+1,而backlog为此队列允许的最大个数,超过此值,则直接将新的连接删除,即不在接收新的连接。当上层调用accept函数接收一个连接(处于请求队列里面的后备连接),队列个数会-1。两个队列的存在,解耦了客户端的连接请求和服务器的连接处理,即解耦了3次握手过程和数据接收过程。
backlog参数
是ESTABLISHED状态队列的最大个数,大多数实现默认值为 5,当服务器把这个完成连接队列的某个连接取走后(accept()取走),这个队列的位置又空出一个,这样来回实现动态平衡,但在高并发 web 服务器中此值显然不够。那么这样一个加一个减,只要底层提交的速度小于上层接收的速度(一般是这样),很明显backlog就不能限制连接的个数,只能限制后备连接的个数。
accept()
从处于 ESTABLISHED状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。
如果,服务器不能及时调用 accept() 取走队列中已完成的连接,队列满掉后会客户端的 connect 就会返回 ETIMEDOUT,连接失败。
select()
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
为了解决连接阻塞或者读写数据阻塞问题,引入select函数。通过select函数检测超时时间(0到无穷大(NULL))段内是否有IO事件发生。
充分利用一次系统调用select就实现多个client事件的管理,大大降低了非阻塞IO频繁无效地使用系统调用。
select 基于fd_set :typedef struct{ long int fds_bits[32]; }fd_set; 32个int,固定128个字节。每个int有32位,32个int有1024位,受fd_set的限制,最多只能接受1024个连接。
int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1;
timeout=
NULL,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
*timeout=
0,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
// 清空fdset与所有文件句柄的联系。 FD_ZERO(fd_set *fdset)
// 建立文件句柄fd与fdset的联系。 FD_SET(int fd, fd_set *fdset)
//清除文件句柄fd与fdset的联系。 FD_CLR(int fd, fd_set *fdset)
//检查fdset联系的文件句柄fd是否可读写,>0表示可读写。 FD_ISSET(int fd, fdset *fdset)
poll()
struct pollfd fds[POLL_SIZE] = {0};
fds[0].fd = listenfd; //指定要监听的fd
fds[0].events = POLLIN;
//指定要监听的fd的事件
int nready = poll(fds, max_fd + 1, -1);
if (fds[0].revents & POLLIN){}//指定的fd有事件发生的时候会放在
revents中返回来(select的是通过fd_set传入内核,有事件的时候更新fd_set然后返回来)
poll 同select差不多,比select好的地方,1.解决了select连接数受限的问题;2.额外使用revents传出事件,所以fd数组只需一次性赋值指定后可重复使用,解决了select的fd_set每次都要被内核更新所以用户程序使用时必须每次都重新再赋值的问题。
poll 和 select的缺点: 都是在程序中定义固定大小的fd数组然后复制拷贝到内核,在内核遍历这个数组检查是否有指定事件发生,然后更新数组之后再拷贝传递到用户程序。用户程序拿到fd的结果数组之后再遍历查找事件然后执行。
epoll()
https://pic3.zhimg.com/v2-cfedb455fdc17882246930d54e61a86e_r.jpg
- 进程通过 epoll_create 创建 eventpoll 对象。
- 进程通过 epoll_ctl 添加关注 listen socket 的 EPOLLIN 可读事件。
- 接步骤 2,epoll_ctl 还将 epoll 的 socket 唤醒等待事件(唤醒函数:ep_poll_callback)通过 add_wait_queue 函数添加到 socket.wq 等待队列。当 listen socket 有链接资源时,内核通过 __wake_up_common 调用 epoll 的 ep_poll_callback 唤醒函数,唤醒进程。
- 进程通过 epoll_wait 等待就绪事件,往 eventpoll.wq 等待队列中添加当前进程的等待事件,当 epoll_ctl 监控的 socket 产生对应的事件时,被唤醒返回。
- 客户端通过 tcp connect 链接服务端,三次握手成功,第三次握手在服务端进程产生新的链接资源。
- 服务端进程根据 socket.wq 等待队列,唤醒正在等待资源的进程处理。例如 nginx 的惊群现象,__wake_up_common 唤醒等待队列上的两个等待进程,调用 ep_poll_callback 去唤醒 epoll_wait 阻塞等待的进程。
- ep_poll_callback 唤醒回调会检查 listen socket 的完全队列是否为空,如果不为空,那么就将 epoll_ctl 监控的 listen socket 的节点 epi 添加到
就绪队列
:eventpoll.rdllist,然后唤醒 eventpoll.wq 里通过 epoll_wait 等待的进程,处理 eventpoll.rdllist 上的事件数据。 - 睡眠在内核的 epoll_wait 被唤醒后,内核通过 ep_send_events 将就绪事件数据,从内核空间拷贝到用户空间,然后进程从内核空间返回到用户空间。
- epoll_wait 被唤醒,返回用户空间,读取 listen socket 返回的 EPOLLIN 事件,然后 accept listen socket 完全队列上的链接资源。
使用分3步:创建epoll,往epoll添加监听事件,等待时间发生。
int epfd = epoll_create(1); //创建
struct epoll_event ev, events[5] = {0};
ev.data.fd = STDIN_FILENO;
ev.data.ptr = userdata_ptr; // 注册的时候带上用户自定义数据userdata,等wait返回的events能拿到userdata,然后可以通过userdata进行相关的操作,比如 userdata 为类对象,返回之后通过类对象调用函数,这样形成一个回调的反馈机制。
ev.events = EPOLLIN;//|EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //定制事件
fd_count = epoll_wait(epfd, events, 5, -1); //等待事件发生
if (events[i].data.fd == STDIN_FILENO){ read() }
红黑树 epfd 的作用:当有事件发生时,可以快速根据fd查找epitem(找到得epiterm会组成链表传递给用户空间做进一步处理),比遍历链表快。
epoll的优势在于epoll不用数组存储事件,而是建立了一个自己的文件系统,把事件都挂载在文件系统上,以红黑树结构存储事件,这样在有很多事件的情况下充分利用了红黑树的高效查改删。
epoll 的性能并不必然比 select 高,对于 fd 数量较少并且 fd IO 都非常繁忙的情况 select 在性能上反而有优势。
fd_install(fd, file); 将文件描述符添加到当前进程的文件描述符表中。
创建enventpoll(相当于子类) 文件的时候同时创建struct file(相当于基类),然后把 struct file 的private_data设置为 enventpoll。通过epfd拿到的是struct file,要再通过private_data才能转换成enventpoll。
f = fdget(epfd);
ep = f.file->private_data;
socket也可以通过fdget获取到socket自己的struct file,然后再通过private_data获取sock自己的文件对象。
注册监听事件,添加红黑树节点。
ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *tfile, int fd, int full_check){//插入一个红黑树节点
struct ep_pqueue epq;
struct epitem *epi;
epi->ep = ep; ep_set_ffd(&epi->ffd, tfile, fd); epi->event = *event;
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);/*注册回到函数*/
revents = ep_item_poll(epi, &epq.pt, 1); //执行 static inline unsigned int ep_item_poll(struct epitem *epi, poll_table *pt) { pt->_key = epi->event.events; return epi->ffd.file->f_op->poll(epi->ffd.file, pt) & epi->event.events; }//ffd.file是要监听的fd(如sockfd)对应的文件对象,即调用sock对象自己检查自己的事件是否到来,这个sockfd和epfd对接的关键点。
ep_rbtree_insert(ep, epi);
if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) { list_add_tail(&epi->rdllink, &ep->rdllist);//把当前event加入readyList的尾部 ep_pm_stay_awake(epi);
/* Notify waiting tasks that events are available 如果网卡收到数据,需要唤醒等待的task,并执行实现设置好的回调函数 */ if (waitqueue_active(&ep->wq)) wake_up_locked(&ep->wq);//唤醒进程后执行的回调函数 if (waitqueue_active(&ep->poll_wait)) pwake++;
if (pwake) ep_poll_safewake(&ep->poll_wait);
}