信号、信号量
信号(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>
int semget(key_t key, int nsems, int semflg);
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>
union semun { int val; struct semid_ds *buf; unsigned short *array; };
int main() { key_t key = ftok(".", 's'); int semid = semget(key, 1, IPC_CREAT | 0666);
if (semid == -1) { perror("semget"); exit(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>
struct sembuf p = { 0, -1, SEM_UNDO }; struct sembuf v = { 0, 1, SEM_UNDO };
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; }
|
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>
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);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict abs_timeout);
int sem_close(sem_t *sem);
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);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
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";
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; }
|
创建后,在 /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";
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; }
|
匿名信号量
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);
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; }
|
参考文档:
本文链接:https://www.zoucz.com/blog/2022/07/19/f4cdbc60-a2dd-11ee-bb9f-1566042b6386/