1. TCP三次握手和监听套接字的两个队列
1.1 未完成队列(SYN_RCVD状态)
当第一次握手之后,在未完成队列上建立条目
1.2 已完成队列(ESTABLISHED状态)
三次握手成功之后,该条目转移到已完成队列,accept能够返回。
int listen(int sockfd, int backlog);
其中backlog这个参数大概为这两个队列总和的最大值,最大值可能是1.5*backlog,这是一个模糊因子。每次握手之间的时间RTT通常为187ms左右。
accept由tcp服务器调用,用于从已完成队列的对头返回下一个已完成的连接,如果队列为空,则阻塞。
2. select的实现机制
accept每次只能返回一个连接,这个连接相应的网络io(socket)是否可读可写我们并不知道,read时很可能会阻塞住。而select可以解决这个问题,我们把关注的io和对应事件,通过select注册,当事件发生时,select会通知我们。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
timeout用来是设置等待时间(永远等待/等待间隔/不等待)
nfds 最大的文件描述符+1
readfds 检测读操作,如果想要接收客户端的数据
writefds 检测写数据的操作,服务器给客户端发数据
excepefds 检测文件描述符是否异常
文件描述符集合的操作
文件描述符集类型: fd_set rdset;
-
void FD_ZERO(fd_set *set); - 全部清空
- void FD_CLR(int fd, fd_set *set) - 从集合中删除某一项
- void FD_SET(int fd, fd_set *set); - 将某个文件描述符添加到集合
- int FD_ISSET(int fd, fd_set *set); - 判断某个文件描述符是否在集合中
select允许进程指示内核等待多个事件中的任何一个发生,并只在一个或多个事件发生之后才唤醒它,否则堵塞休眠。换句话说就是,我们调用select时,可以通过参数告知内核对哪些描述符(读、写或者异常)感兴趣以及要等多长时间。
windows和linux上对select都有实现,可以说这是一个跨平台的函数。
但是它的缺点也很明显
- 调用select需要把fd集合从用户态拷贝到内核态,开销大
- 同时也需要在内核遍历传递所有fd,开销大
- select支持的文件描述符数量很少,默认是1024,这对于高并发很不友好
3. epoll的实现机制
epoll可以看作是Linux在select/poll或者说io多路复用的一个重大改进,有了它之后,才是Linux在网络操作系统上占据重要份额。
3.1 epoll的数据结构
int epoll_create(int size);
epoll_create()创建一个新的epoll实例。返回参数为epoll示例的文件描述符,参数max_size 从Linux 2.6.8开始被忽略,但必须大于零。使用完epoll后,要close()掉,否则会造成fd资源泄漏。
这个实例核心的两个数据结构是要给红黑树和一个双向链表。红黑树位于内核空间管理 socket,双向链表存放就绪的 socket。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl 用于控制epoll实例事件,可以注册、修改、删除关注的socket。
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
epoll_wait 等待IO事件发生,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。返回就绪队列,还有队列的长度。这样就知道具体的哪些fd就绪了。
3.2 线程安全
epoll是线程安全的,它是通过锁来实现的。
3.3 ET和LT
EPOLL事件有两种模型:
Edge Triggered(ET) // 边沿触发,数据来了就触发一次,只支持非阻塞io
LevelTriggered(LT) //缺省方式,水平触发,有数据就出发,支持非阻塞io和阻塞io
3.4 EPOLLONESHOT
在多线程服务中,一个网络io到来时,数据开始处理,这时候这个网络io也就是socket又来了一个同样的事件,那么另一个线程来处理新的事件,这就会出现一个问题,不同的线程处理同一个socket,这会让程序变得复杂。
EPOLLONESHOT就是来解决这个问题的,在epoll上注册这个事件后,如果已经在处理中的socket,将不再重新注册相关事件。这样就保证了同一SOCKET只能被一个线程处理,避免了上述问题。
参考:
[1] https://www.cnblogs.com/love-yh/p/7425031.html
[2] UNIX网络编程
[3] https://blog.youkuaiyun.com/u013051748/article/details/108804064
[4] https://zhuanlan.zhihu.com/p/165162146