1、阻塞IO
服务器端:bind listen accept read
客户端:connect(向服务器发起请求)write
服务端阻塞在了两个地方
一是accept的三次握手
而是read函数
read函数展开阻塞分为两个部分:一是从网卡传到内核缓冲区,二是从内核缓冲区拷贝到用户缓冲区
整体流程如下:
所以客户端一直不发数据,服务端就会阻塞在read上
2、非阻塞IO
(read函数可以配置成非阻塞模式)
首先从用户层上简单介绍:就是一种方法是每次都创建一个新的线程去调用read函数并做业务处理,
pthread_creat(dowork);
void dowork(){
read();
...
}
操作系统配置的非阻塞read函数的效果是:
如果数据没有到达时(就是客户端的数据没有到达网卡并拷贝到内核缓冲区),立刻返回一个错误值(-1)而不是阻塞等待。
设置方法:
fcntl(connfd, F_SETFL, O_NONBLOCK);
数据没到达时就是非阻塞的到达以后就是阻塞的。
3、IO多路复用
1>为每个客户端创建一个线程,服务器端的资源很容易被耗光
2>服务器每accept一个客户端连接后将文件描述符connfd = accept(listenfd)放到一个数组里然后新建一个线程去遍历
while(1) {
for(fd <-- fdlist) {
if(read(fd) != -1) {
doSomeThing();
}
}
}
每次遍历遇到 read 返回 -1 时仍然是一次浪费资源的系统调用。(这些只是方便理解内核的处理方式)
还是得恳请操作系统老大,提供给我们一个有这样效果的函数,我们将一批文件描述符通过一次系统调用传给内核,由内核层去遍历,才能真正解决这个问题。
4、select
select是操作系统提供的函数
通过它,我们可以把一个文件描述符的数组发给操作系统, 让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理:
int select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
// nfds:监控的文件描述符集里最大文件描述符加1
// readfds:监控有读数据到达文件描述符集合,传入传出参数
// writefds:监控写数据到达文件描述符集合,传入传出参数
// exceptfds:监控异常发生达文件描述符集合, 传入传出参数
// timeout:定时阻塞监控时间,3种情况
// 1.NULL,永远等下去
// 2.设置timeval,等待固定时间
// 3.设置timeval里时间均为0,检查描述字后立即返回,轮询
服务端代码,这样来写。
首先一个线程不断接受客户端连接,并把 socket 文件描述符放到一个 list 里。
while(1) {
connfd = accept(listenfd);
fcntl(connfd, F_SETFL, O_NONBLOCK);
fdlist.add(connfd);
}
然后,另一个线程不再自己遍历,而是调用 select,将这批文件描述符 list 交给操作系统去遍历。
while(1) {
// 把一堆文件描述符 list 传给 select 函数
// 有已就绪的文件描述符就返回,nready 表示有多少个就绪的
nready = select(list);
...
}
不过,当 select 函数返回后,用户依然需要遍历刚刚提交给操作系统的 list。
只不过,操作系统会将准备就绪的文件描述符做上标识,用户层将不会再有无意义的系统调用开销。
while(1) {
nready = select(list);
// 用户层依然要遍历,只不过少了很多无效的系统调用
for(fd <-- fdlist) {
if(fd != -1) {
// 只读已就绪的文件描述符
read(fd, buf);
// 总共只有 nready 个已就绪描述符,不用过多遍历
if(--nready == 0) break;
}
}
}
总结:
1. select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
2. select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)
3. select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)
可以看到,这种方式,既做到了一个线程处理多个客户端连接(文件描述符),又减少了系统调用的开销(多个文件描述符只有一次 select 的系统调用(描述符数组传到内核) + n 次就绪状态的文件描述符的 read 系统调用)
5、poll
poll 也是操作系统提供的系统调用函数。
int poll(struct pollfd *fds, nfds_tnfds, int timeout);
struct pollfd {
intfd; /*文件描述符*/
shortevents; /*监控的事件*/
shortrevents; /*监控事件中满足条件返回的事件*/
};
它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。