操作系统 2:信号与信号量
信号
信号是内核向进程发出的消息,用来通知进程「系统中发生了某种类型的事件」。信号可以是内核自身产生,也可以由某一个进程产生,籍由内核发送到另一个进程。信号分为不同的种类,每种信号都由一个整数 ID 来表示。在 *nix 系统中,使用 kill -l
查看所有信号列表:
信号的接收
当目的进程被内核强制以某种方式对信号的发送做出反应时,它就接收了这个信号。进程可能有下面的「反应」:
忽略信号:什么也不做。
终止 / 停止:终止或挂起当前进程。还可能转储内存到文件以供调试。
执行一个信号处理程序。
类似中断的处理,内核有一套信号的处理与阻塞机制。在每个进程的上下文中,内核维护有挂起和阻塞向量——当信号 \(k\) 被传输来时,挂起向量第 \(k\) 位拉高;当信号被接收时拉低。而阻塞位拉高位的信号,则信号不会被接收(但也不会消失),直到阻塞取消,信号才会被处理。
特别地,信号不会排队(多个同类信号到来时,后来者被丢弃),而内核默认阻塞与当前正在处理信号同类的信号。
信号处理程序的设置
我们使用 signal()
函数设置某个信号的处理例程。函数原型是:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
例如,signal(SIGINT, foobar)
将在程序收到 SIGINT
时,调用函数 foobar()
。
安全的信号处理
下面是编写信号处理程序(函数)的一些注意事项。
处理程序尽可能简单。
在处理程序中,只使用异步信号安全的函数。
保存和恢复
errno
。阻塞所有信号。
用
volatile
声明全局变量。
信号量
信号量是一种具有非负整数值的全局变量,它有两种操作 P
和 V
:
P(s)
:如果s
非零,就将s
减 1,然后立即返回。此操作是原子的,即它要么不做要么必须做完。如果s
是零,则阻塞当前进程,直到s
因为一个V
操作增加。或者,参考下面的定义:
wait(semaphore_t *s) { atomic s->value--; if (s->value < 0) { add current process to s->list; block(); } }
V(s)
:将s
加 1。此操作是原子的。同时,如果s
此时正造成某个进程阻塞,那么将那个进程唤醒。或者,参考下面的定义:
signal(semaphore_t *s) { atomic s->value++; if (s->value <= 0) { remove process p from s->list; wakeup(p); } }
对于值总是 0 或 1 的信号量,称为二元信号量。以提供互斥操作为目的的二元信号量称为互斥锁。以资源计数为目的的信号量称为计数信号量。
信号量的使用
引入信号量的目的是将每个共享变量与一个信号量联系起来,利用信号量非负的性质,将临界区「包」起来,保证多个进程不能同时占有共享变量。P
操作相当于「加锁」,而 V
操作相当于「解锁」。
例如,有共享变量
volatile int cnt = 0;
现在有多个进程都可能写入它。我们可以为这个变量定义和初始化一个互斥锁:
sem_t mutex;
sem_init(&mutex, 0, 1 /* initial value */ );
当需要操作 cnt
时,用 P
和 V
操作加锁、解锁:
P(&mutex);
cnt++;
V(&mutex);
死锁
死锁是一种因多个进(线)程循环等待资源造成无法执行的恶性局面。例如:
甲:我有故事给我酒
乙:我有酒给我故事
结果甲一直在等酒,乙一直在等故事,双方僵持。
互斥使用:至少有一个资源的使用是互斥的。
不可抢占:资源只能自愿放弃。
持有和等待:进程必须占有资源,然后再申请。
循环等待:在资源分配图中存在一个环路。