接上篇linux IO —— 阻塞IO

阻塞IO的问题是单进程(单个线程)同时只能处理一个fd上的IO。如果有多个客户端连接上来,要么就等待前面的连接处理完毕断开;要么就每个连接单独启动一个线程来处理IO任务,然而启动线程的开销是比较大的,受到操作系统线程数量、栈空间分配内存等资源限制。

此时就可以考虑使用非阻塞IO,即让程序在等待IO操作的时候不进入阻塞状态,这样就可以循环遍历多个fd,来完成多个连接的IO任务了。

设置socket fd为非阻塞

fcntl 函数

不管是服务端的 监听 socket,数据 socket,还是客户端的 socket,都可以通过 fcntl 设置为非阻塞模式。

1
2
3
4
5
//接收到了连接
int client_fd = accept(sfd, (sockaddr*)&addr, &len);
//将连接的fd设置为非阻塞
int origin_flags = fcntl(client_fd, F_GETFL, 0);
fcntl(client_fd, F_SETFL, origin_flags|O_NONBLOCK);

type 参数

监听端口的过程是先创建一个 socket,再 bind 地址,然后叫给 listen 函数去监听。

1
2
3
4
5
int sfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
//绑定fd和addr
if( bind(sfd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1 ) handle_error("bind inet error");
//监听fd
if( listen(sfd, LISTEN_BACKLOG) == -1 ) handle_error("listen inet error");

客户端连接远端的过程也类似,创建 socket,设置地址,然后交给 connect 函数去建立连接。

1
2
3
sockaddr_in remote_addr;
int cfd = socket(AF_INET, SOCK_STREAM|SOCK_NONBLOCK, 0);
int ret = connect(cfd, (sockaddr*)&remote_addr, sizeof(remote_addr));

根据 socket 文档,可以在创建的时候设置非阻塞 type,即 SOCK_NONBLOCK。
实际上很多涉及fd创建的函数,都支持非阻塞的 type。
设置为非阻塞 fd 后,监听 socket 在调用 accept 的时候,不再进入阻塞状态;客户端 socket 在 connect 的时候,也不再进入阻塞状态。

非阻塞IO的响应

与阻塞IO相比,非阻塞IO执行相关IO函数不满足条件时,不再阻塞,而是直接失败,并设置相关的 errno。

  • accept:返回-1,errno设置为 EAGAIN 或者 EWOULDBLOCK
  • connect:返回-1,errno设置为 EAGAIN(unix domain socket)、EINPROGRESS/EALREADY(TCP)
  • read:返回-1,errno设置为 EAGAIN 或者 EWOULDBLOCK,如果是文件fd,则只会返回EAGAIN
  • write:返回-1,errno设置为 EAGAIN 或者 EWOULDBLOCK,如果是文件fd,则只会返回EAGAIN

单进程(单线程)处理多路连接的示例

详见:https://github.com/zouchengzhuo/linux-io-learn/tree/master/2.nonblocking

server端实现一个非阻塞的监听socket,不断accept客户端连接,并将连接放到一个fd数组中;同时不断遍历fd数组,尝试从其中read数据,若有断开的fd,则将fd从fd数组中移除。

client端实现一个非阻塞socket连接到服务端,不断遍历并发送数据,然后读取数据。

image.png

上图是运行结果,最左侧是server端的输出,中间是client端的输出,右侧是资源使用数据。

可以看到,单进程成功处理了多个连接的请求,但是因为程序在不断遍历并判断,客户端和CPU都占满了一个核的CPU资源。

☞ 参与评论