众所周知,进程会在内核态切换到用户态的时候处理信号,那么如果进程在 I/O 的过程中收到了信号会怎么样?
以 Linux 为例,一个进程会有如下状态。
1
2
3
4
5
6
7
8
9
// Linux内核中的进程状态定义
#define TASK_RUNNING 0 // 运行或就绪
#define TASK_INTERRUPTIBLE 1 // 可中断睡眠
#define TASK_UNINTERRUPTIBLE 2 // 不可中断睡眠
#define __TASK_STOPPED 4 // 停止状态
#define __TASK_TRACED 8 // 被跟踪状态
#define EXIT_DEAD 16 // 退出状态
#define EXIT_ZOMBIE 32 // 僵尸状态
#define TASK_DEAD 64 // 死亡状态
一个进程调用 I/O 函数后可能有三种情况:阻塞等待、正在 I/O 和时间片用完。
一、阻塞等待
阻塞等待期间进程可能会进入两种状态:可中断睡眠和不可中断睡眠。一般情况下都会进入可中断睡眠,只有在等待必须完成的关键 I/O 操作时才会进入不可中断睡眠,例如驱动程序、内存页换入和文件系统元数据操作。
这两种状态在收到信号时的反应如下:
可中断睡眠:进程被唤醒,状态被修改为 TASK_RUNNING,I/O的系统调用返回 -1,errno 被设置为 EINTR,然后进程可以处理此信号。
不可中断睡眠:信号被标记为 pending,进程会继续等待 I/O 操作,直到 I/O 完成之后进程才会被唤醒并在返回用户态的时候处理 pending 信号。
二、正在 I/O
考虑到 UDP 和 TCP 的性质不同,正在 I/O 的进程也需要分两种情况讨论:读取 UDP 数据和读取 TCP 数据。
读取 UDP 数据:这种情况比较简单,有数据时进程会一次读取一个报文,然后就会在切换到用户态时处理信号。信号不会中断 I/O 操作。
读取 TCP 数据:TCP 需要考虑内核缓冲区只有部分数据的情况,但是信号对读取操作的影响也是几乎不存在的。信号仍然不会中断读取操作,而是会在读取操作完成之后返回用户态的时候被处理。
上面虽然对两种情况分开讨论,但其实这两种情况在内核缓冲区中有数据时对信号处理的行为是一致的。
三、时间片用完
考虑到在多核 CPU 中进程可能会被频繁的换入换出,如果进程刚读取一个 UDP 报文的一半数据时 CPU 时间片用完了,这个时候进程就会被放在就绪队列的队尾等待下一个时间片的到来,整个过程进程都是在 TASK_RUNNING 状态的。如果进程刚被放到就绪队列的队尾,恰好此时收到了一个信号会怎么样?
答案就是:信号被标记为 pending,并不会唤醒进程。信号之后读取操作完成之后返回用户态的时候才能被处理。
结语:进程在 I/O 操作时收到信号是一种比较复杂的场景,不过操作系统会尽量考虑到所有可能性并且保证最终结果的正确。