Tcp 的网络模型
- socket 创建套接字,他是系统给应用程序的一个文件描述符
- 对系统的bind,绑定地址和端口号
- 系统调用listen创建一个队列用于存放客户端进来的连接
- 最后使用accep进行连接请求。
堵塞式的IO
在socket默认状态之下堵塞的,主要有三种堵塞:
-
connect 阻塞:
当客户的发起TCP请求,是通过connect函数,使用TCP连接建立的需要完成三次握手过程。在ack和syn信号需要堵塞等待。 -
Accept阻塞,一个堵塞的socket通信的服务端接受外来连接,会调用accept,没有新的连接到达,调用进程会将你过期。
-
read,write 堵塞,当一个sock创建成功之后,度无端用fork函数创建一个子进程,调用read函数等待客户端的数据写入,如果没有数据写入没调用子进程挂起,进入堵塞状态。
非堵塞式IO
使用fcntl可以把以上三种方式设置为为非堵塞状态。如果没有数据返回,就会直接返回一个EWOULDBLOCK 或 EAGAIN 错误。
当我们把以上操作设置为了非堵塞状态,需要一个线程会该操作进行轮询检查。
IO复用
用一个线程进行进行三种状态的轮询对于大量请求情况下,对cpu而言是一总灾难。
Linux 提供了IO复用函数select、poll、epoll,进程将一个或者多个读操作通过系统调用函数堵塞在函数操作欧尚。使用系统内核见识读操作是否就绪。
select函数
在超时时间内,监听用户感兴趣文件描述符上的可读可写和异常时间的发生。Linux内核把所有的外部设备看做是一个文件来操作,对于一个文件的读写操作会调用内核提供的系统命令,返回一个文件描述符。
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
select 函数监视文件描述符就绪或者超时,函数返回。当select函数返回后,通过FD_ISSET遍历fdset,找到这个描述符。「其中fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可以通过四个宏获得」
void FD_ZERO(fd_set *fdset); // 清空集合
void FD_SET(int fd, fd_set *fdset); // 将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); // 将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
poll函数
在每次调用select函数之前,系统需要把fd从用户态拷贝到内核态,这会对系统带来一定的开销。对于单个进程监视的fd数量默认是1024,我们可以通过修改宏定义甚至重新编译内核的方式打破这一限制。对于fd_set是基于数组实现的,在add和delete
fd的时候,数量过大效率降低。
poll 和 select 的区别联系
poll 和select 存在一个相同的缺点是需要拷贝文件描述符被整体赋值到用户态和内核态之间,而无论这些这些文件是都准备就绪,他们开销就会随着文件描述符数量线程增大。(poll监视的文件符没有select 的1024数量的限制,但是由于他会把fd拷贝到内核态,会带来一定开销)
epoll 函数
select / poll 是顺序扫描fd是否就绪,而且支持的fd数量不宜过大,因此会有制约。
对于epoll,使用的是事件驱动的方式代替轮询扫描fd。epoll事先通过epoll_ctl()来注册一个文件描述符,将文件描述符存放到内核的一个时间表中,这个时间表基于红黑树实现,所以在大量的IO请求场景下,插入和删除的性能会比前两个号。因此,epoll的性能更好,不受到fd数量的限制。
void FD_ZERO(fd_set *fdset); // 清空集合
void FD_SET(int fd, fd_set *fdset); // 将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); // 将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
网友评论