信号、信号量

信号(signal)和信号量(semaphore)都是用于进程间通信和同步的机制,在中文翻译下名字很相近,但它们之间的区别其实非常大。

目的与用途

  • 信号(signal):信号是一种异步的通知机制,用于通知进程某个事件发生了。信号可以由操作系统内核发送,也可以由其他进程发送。信号通常用于处理异常情况(如终止进程、处理中断等)和进行进程间通信。信号不携带任何附加信息,只是一个事件通知。

  • 信号量(semaphore):信号量主要用于同步和保护多个进程或线程之间对共享资源的访问。信号量是一个计数器,可以用来控制同时访问某个资源的进程或线程的数量。当信号量的计数器大于0时,进程可以访问共享资源;当计数器为0时,进程需要等待其他进程释放资源。

实现机制

  • 信号量(semaphore):信号量可以是System V信号量或POSIX信号量。它们通常由专门的系统调用实现,如semget、semop(System V)或sem_init、sem_wait、sem_post(POSIX)等。信号量可以是二进制的(只有0和1两个状态)或者是具有更大范围的计数器。

  • 信号(signal):信号由操作系统内核实现,使用一组系统调用来处理,如kill、sigaction、sigprocmask等。信号有许多预定义的类型(如SIGINT、SIGTERM、SIGSEGV等),每种类型对应一个特定的事件。进程可以定义信号处理函数来处理接收到的信号。

同步与异步

  • 信号(signal):信号是异步的。当一个进程接收到一个信号时,它会中断当前的操作并立即处理信号。信号处理函数在完成后,进程会恢复执行被中断的操作。
  • 信号量(semaphore):信号量通常用于同步操作。当进程试图访问一个受信号量保护的共享资源时,它需要等待信号量的计数器大于0。这意味着进程可能会阻塞,直到资源可用。

总之,信号量主要用于同步和保护共享资源的访问,而信号主要用于异步通知和异常处理。这两种机制在实现和用途上有很大的不同。

PV 操作原语

PV原语通过操作信号量来处理进程间的同步与互斥的问题。其核心就是一段不可分割不可中断的程序。 信号量的概念1965年由著名的荷兰计算机科学家Dijkstra提出,其基本思路是用一种新的变量类型(semaphore)来记录当前可用资源的数量。

semaphore有两种实现方式:

  • semaphore的取值必须大于或等于0。0表示当前已没有空闲资源,而正数表示当前空闲资源的数量;
  • semaphore的取值可正可负,负数的绝对值表示正在等待进入临界区的进程个数。

信号量是由操作系统来维护的,用户进程只能通过初始化和两个标准原语(P、V原语)来访问。初始化可指定一个非负整数,即空闲资源总数。

  • P原语:P是荷兰语Proberen(测试)的首字母。为阻塞原语,负责把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。操作为:申请一个空闲资源(把信号量减1),若成功,则退出;若失败,则该进程被阻塞;

  • V原语:V是荷兰语Verhogen(增加)的首字母。为唤醒原语,负责把一个被阻塞的进程唤醒,它有一个参数表,存放着等待被唤醒的进程信息。操作为:释放一个被占用的资源(把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒之。

P原语操作的动作是:

  • sem减1;
  • 若sem减1后仍大于或等于零,则进程继续执行;
  • 若sem减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。

V原语操作的动作是:

  • sem加1;
  • 若相加结果大于零,则进程继续执行;
  • 若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。

PV操作对于每一个进程来说,都只能进行一次,而且必须成对使用。在PV原语执行期间不允许有中断的发生。

systemv sem

概念

信号量就是内核实现的一个计数器,可以对计数器做加减操作,并且操作时遵守一些基本操作原则,即:对计数器做加操作立即返回,做减操作要检查计数器当前值是否够减(减被减数之后是否小于0)如果够,则减操作不会被阻塞;如果不够,则阻塞等待到够减为止。

相关函数

1
2
3
4
5
6
7
8
9
10
#include <sys/sem.h>
// 创建新信号量set或获取现有set的id
int semget(key_t key, int nsems, int semflg);
// 修改信号量(正值操作、负值操作、0值操作)
int semop(int semid, struct sembuf *sops, size_t nsops);
// 修改信号量,支持传入等待超时时间
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
const struct timespec *_Nullable timeout)
;

// 查看、修改属性等操作
int semctl(int semid, int semnum, int cmd, ...);

示例代码

下面是使用 systemv 信号量实现进程间资源访问同步的示例代码

创建信号量

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
#include <sys/ipc.h>

// 定义一个联合体,用于 semctl 初始化信号量
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};

int main() {
key_t key = ftok(".", 's'); // 生成一个 key,用于创建信号量
int semid = semget(key, 1, IPC_CREAT | 0666); // 创建一个信号量集,包含一个信号量

if (semid == -1) {
perror("semget");
exit(1);
}

// 初始化信号量值为 1
union semun sem_union;
sem_union.val = 1;
if (semctl(semid, 0, SETVAL, sem_union) == -1) {
perror("semctl");
exit(1);
}

printf("Semaphore created with value: %d\n", sem_union.val);
return 0;
}

使用信号量做进程间资源访问同步

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
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/wait.h>

// 定义一个 sembuf 结构体,用于操作信号量
struct sembuf p = { 0, -1, SEM_UNDO }; // p 操作,表示申请资源
struct sembuf v = { 0, 1, SEM_UNDO }; // v 操作,表示释放资源

void pv(int semid, int op) {
struct sembuf sem_op = { 0, op, SEM_UNDO };
if (semop(semid, &sem_op, 1) == -1) {
perror("semop");
exit(1);
}
}

int main() {
key_t key = ftok(".", 's');
int semid = semget(key, 0, 0); // 获取已经创建好的信号量

if (semid == -1) {
perror("semget");
exit(1);
}

pid_t pid = fork(); // 创建子进程

if (pid == 0) {
// 子进程
printf("Child process wants to access shared resource.\n");
pv(semid, -1); // 子进程申请信号量资源
printf("Child process accesses shared resource.\n");
sleep(2); // 模拟子进程访问共享资源
printf("Child process releases shared resource.\n");
pv(semid, 1); // 子进程释放信号量资源
} else {
// 父进程
printf("Parent process wants to access shared resource.\n");
pv(semid, -1); // 父进程申请信号量资源
printf("Parent process accesses shared resource.\n");
sleep(2); // 模拟父进程访问共享资源
printf("Parent process releases shared resource.\n");
pv(semid, 1); // 父进程释放信号量资源
wait(NULL); // 等待子进程结束
}

return 0;
}

image.png

posix semaphore

概念

posix 信号量实现的更清晰简洁,相比之下,systemv 信号量更加复杂,但是却更佳灵活,应用场景更加广泛。在 systemv 信号量中,对计数器的加和减操作都是通过 semop 方法和一个 sembuff 的结构体来实现的,但是在 posix 中则给出了更清晰的定义:使用sem_post函数可以增加信号量计数器的值,使用sem_wait可以减少计数器的值。如果计数器的值当前是0,则sem_wait操作会阻塞到值大于0。

POSIX信号量也提供了两种方式的实现,命名信号量和匿名信号量。

相关函数

命名信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>

// 传入 /somename 格式的名称以创建命名,相同名称可以在进程间共享
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,
mode_t mode, unsigned int value);
// 增加信号量
int sem_post(sem_t *sem);
// 减少信号量,信号量是0时阻塞
int sem_wait(sem_t *sem);
// 减少信号量,信号量是0时返回错误码
int sem_trywait(sem_t *sem);
// 减少信号量,信号量是0时阻塞一段时间后返回错误码
int sem_timedwait(sem_t *restrict sem,
const struct timespec *restrict abs_timeout)
;

// 当前进程释放该信号量
int sem_close(sem_t *sem);
// 删除信号量,此方法立即返回,并当所有使用它的进程 close 掉后删除信号量
int sem_unlink(const char *name);

匿名信号量(基于内存的信号量)

基于内存的信号量必须存储在其共享范围内的共享内存中,如线程共享的信号量,需要存储在全局对象中;进程共享的信号量,需要存放在通过 systemv shm 或者 posix shm 创建的共享内存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <semaphore.h>
// 初始化匿名信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 增加信号量
int sem_post(sem_t *sem);
// 减少信号量,信号量是0时阻塞
int sem_wait(sem_t *sem);
// 减少信号量,信号量是0时返回错误码
int sem_trywait(sem_t *sem);
// 减少信号量,信号量是0时阻塞一段时间后返回错误码
int sem_timedwait(sem_t *restrict sem,
const struct timespec *restrict abs_timeout)
;

// 销毁信号量
int sem_destroy(sem_t *sem);

示例代码

命名信号量

首先创建一个 posix 命名信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/stat.h>

int main() {
const char *sem_name = "/my_semaphore"; // 定义信号量的名称

// 创建一个 POSIX 命名信号量,并初始化为 1
sem_t *sem = sem_open(sem_name, O_CREAT, S_IRUSR | S_IWUSR, 1);

if (sem == SEM_FAILED) {
perror("sem_open");
exit(1);
}

printf("Semaphore created with name: %s\n", sem_name);
sem_close(sem); // 关闭信号量
return 0;
}

image.png

创建后,在 /dev/shm 目录下可以找到刚刚创建的命名信号量

然后使用刚刚创建的信号量做进程同步

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
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/wait.h>
#include <fcntl.h>

int main() {
const char *sem_name = "/my_semaphore"; // 信号量的名称

// 打开已经创建好的 POSIX 命名信号量
sem_t *sem = sem_open(sem_name, O_RDWR);

if (sem == SEM_FAILED) {
perror("sem_open");
exit(1);
}

pid_t pid = fork(); // 创建子进程

if (pid == 0) {
// 子进程
printf("Child process wants to access shared resource.\n");
sem_wait(sem); // 子进程申请信号量资源
printf("Child process accesses shared resource.\n");
sleep(2); // 模拟子进程访问共享资源
printf("Child process releases shared resource.\n");
sem_post(sem); // 子进程释放信号量资源
} else {
// 父进程
printf("Parent process wants to access shared resource.\n");
sem_wait(sem); // 父进程申请信号量资源
printf("Parent process accesses shared resource.\n");
sleep(2); // 模拟父进程访问共享资源
printf("Parent process releases shared resource.\n");
sem_post(sem); // 父进程释放信号量资源
wait(NULL); // 等待子进程结束
}

sem_close(sem); // 关闭信号量

if (pid > 0) {
sem_unlink(sem_name); // 删除信号量
}

return 0;
}

image.png

匿名信号量

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
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/wait.h>
#include <sys/mman.h>

int main() {
// 创建共享内存
sem_t *sem = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

// 初始化匿名信号量,其值为 1
if (sem_init(sem, 1, 1) == -1) {
perror("sem_init");
exit(1);
}

pid_t pid = fork(); // 创建子进程

if (pid == 0) {
// 子进程
printf("Child process wants to access shared resource.\n");
sem_wait(sem); // 子进程申请信号量资源
printf("Child process accesses shared resource.\n");
sleep(2); // 模拟子进程访问共享资源
printf("Child process releases shared resource.\n");
sem_post(sem); // 子进程释放信号量资源
} else {
// 父进程
printf("Parent process wants to access shared resource.\n");
sem_wait(sem); // 父进程申请信号量资源
printf("Parent process accesses shared resource.\n");
sleep(2); // 模拟父进程访问共享资源
printf("Parent process releases shared resource.\n");
sem_post(sem); // 父进程释放信号量资源
wait(NULL); // 等待子进程结束
}

sem_destroy(sem); // 销毁信号量
munmap(sem, sizeof(sem_t)); // 释放共享内存

return 0;
}

image.png

参考文档:

☞ 参与评论