IO任务

IO任务即程序从文件中读取数据,或者向文件中写入数据的过程,这个过程一般是需要CPU来控制的。
例如一个网络IO任务:

1
2
3
4
5
6
7
8
9
10
11
12
//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)

或者一个本地文件IO任务:

1
2
3
4
//打开文件,获取fd
open(...);
//读取内容
read(...);

有些情况下,IO操作无法立即完成。例如网络IO中,进程调用 accept 来尝试接收客户端连接,但是此时并没有客户端发起连接,进程怎么处理 accept 函数的执行呢?
此时就涉及到了阻塞和非阻塞的概念。

阻塞与中断程序

阻塞

image.png

(此部分配图来自https://zhuanlan.zhihu.com/p/63179839)

CPU在执行进程任务时,会将计算资源按时间片划分,需要使用CPU的进程在工作队列中排队,CPU从队列中轮流取出进程,分配时间片并执行任务。
假设此时进程A执行了 accept 函数,但是客户端并没有发起连接,那么如果不采取措施,进程A就会一直在工作队列中,一直卡在 accept 函数执行的地方,CPU的使用率为 100%。

所以此时系统需要采取一些策略,以避免无意义的计算资源消耗。
image.png

当执行 accept 函数,但是客户端没有发起连接时,系统将进程A从工作队列中移除,放到 accept 的 socketfd 的等待队列中,此时CPU执行任务时,不会再将时间片分配给进程A。
我们称进程A进入了 阻塞状态
那么什么时候恢复进程A的执行呢?

中断程序

image.png
在客户端发起连接,网卡收到数据后,会执行一个中断程序,打断当前CPU正在执行的任务,主要做两件事情:1.将网卡收到的数据拷贝到内核缓冲区(即socket的接收缓冲区)2.将进程A放回执行队列。
CPU再次执行进程任务时,accept 函数就能拿到客户端连接数据了,程序继续正常执行。

现在再来理解一下阻塞:

  • 对单个进程而言是低效的,一旦进入阻塞状态,进程就无法执行其它任务了
  • 对整个系统而言想高效的,让不必要继续执行的进程进入阻塞状态,让出CPU,提升整个系统的处理能力

一些涉及阻塞的操作

有不少可能导致进程阻塞的操作,这里列举一小部分本文涉及的,后面会整理出更多。

  • accept函数
  • read函数
  • write函数
  • connect函数

    示例

    监听
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int ListenINETPort(){
    //监听的 inet 地址结构体
    struct sockaddr_in inet_addr;
    inet_addr.sin_family = AF_INET;
    inet_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    inet_addr.sin_port = htons(8080);
    //创建监听socket,常用类型 SOCK_STREAM-tcp SOCK_DGRAM-udp SOCK_RAW-icmp,可以叠加SOCK_NONBLOCK将socket设置为非阻塞模式,如 SOCK_STREAM|SOCK_NONBLOCK
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    //inet地址绑定到socket
    if( bind(sfd, (struct sockaddr *)&inet_addr, sizeof(inet_addr)) == -1 ) handle_error("bind inet error");
    //监听,第二个参数定义积压的连接数,例如这里定义为2,那么超过2个积压后,客户端可以收到 ECONNREFUSED 错误
    if(listen(sfd, LISTEN_BACKLOG) == -1) handle_error("listen");
    return sfd;
    }

接收连接,读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int AcceptAndRecvINET(int sfd){
//接收连接的循环
while(1){
//创建接收的socket地址
sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
//接收客户端连接
int cfd = accept(sfd, (struct sockaddr*)&client_addr, &client_addr_length);
if(cfd == -1){
printf("accept fail: %d", errno);
return 0;
}
printf("accept connect success: %d", cfd);
//定义一个缓冲区数据块,用于接收客户端的数据
char buf[BUFFER_SIZE];
//接收数据的循环
while (1)
{
bzero(buf, 0);
//接收客户端的数据,最后一个参数是flags,用于控制接收消息的模式
int recvbytes = recv(cfd, buf, sizeof(buf), 0);
//客户端断开
if(recvbytes == 0){
printf("client is disconnect: %d \n", cfd);
break;
}
//接收错误,继续
if(recvbytes < 0){
printf("recv error: %d", errno);
continue;
}
//打印结果
printf("client->server: %s \n", buf);
//原样返回
send(cfd, buf, sizeof(buf), 0);
}
close(cfd);
}
close(sfd);
}

启动服务端程序,并启动多个客户端程序发起连接然后发送数据

image.png

可以看到,阻塞io下,一个进程只能同时处理一个连接,其它连接需要等待前面的连接断开后才能被处理。

示例代码:https://github.com/zouchengzhuo/linux-io-learn/tree/master/1.blocking

☞ 参与评论