1. 信号量的相关概念
• 信号量(比较底层)是一种广泛使用的同步机制 ,用于控制对共享资源的访问。用来解决多个进程或线程 间的同步与互斥问题。
• 与共享存储等不同,在 Linux 中,信号量是用来协调进程或线程的执行的,并不承担传输数据的职责。
1.1 基本概念
• 信号量本质上是一个非负整数变量 ,可以被用来控制对共享资源的访问。它主要用于两种目的:互斥和同步。
• 互斥(Mutex):确保多个进程或线程不会同时访问临界区(即访问共享资源的代码区域),这个就有点类似与互斥锁。
• 同步(Synchronization):协调多个进程或线程的执行顺序 ,确保它们按照一定的顺序执行,这个就有点类似于条件变量。
1.2 基于用途的分类
• 基于不同的目的,信号量可以分为两类:用于实现互斥的"二进制信号量"和用于同步的"计数信号量"。
• 二进制信号量(或称作互斥锁):其值只能是 0 或 1,主要用于实现互斥,即一次只允许一个线程进入临界区。通常用于控制共享资源的访问,避免竞态条件的产生。
• 计数信号量:其值可以是任意非负整数,表示可用资源的数量 。计数信号量允许多个线程根据可用资源的数量进入临界区 。通常用于控制不同进程或线程执行的顺序,如消费者必须在生产者发送数据后才可以消费。
1.3 基于名称的分类
• 在 Linux 中,根据是否具有唯一的名称,分为有名信号量(named semaphore)和无名信号量(unnamed semaphore)。这两种信号量特性有所不同:
• 无名信号量:无名信号量不是通过名称标识,而是直接通过 sem_t 结构的内存位置标识 。无名信号量在使用前需要初始化,在不再需要时应该销毁。它们不需要像有名信号量那样进行创建和链接,因此设置起来更快,运行效率也更高。
• 有名信号量:有名信号量在系统范围内是可见的,可以在任意进程之间进行通信。它们通过名字唯一标识 ,这使得不同的进程可以通过这个名字访问同一个信号量对象 。在当前 Linux 系统中,有名信号量在临时文件系统中的对应的文件位于/dev/shm 目录下,创建它们时可以像普通文件一样设置权限模式,限制不同用户的访问权限。
1.4 操作,信号量主要提供了两个操作:P 操作和 V 操作:
• P 操作(Proberen,尝试):也称为等待操作(wait),用于减少信号量的值。如果信号量的值大于 0,它就减 1 并继续执行;如果信号量的值为 0,则进程或线程阻塞,直到信号量的值变为非零。
• V 操作(Verhogen,增加):也称为信号操作(signal),用于增加信号量的值。 如果有其他进程或线程因信号量的值为 0 而阻塞,这个操作可能会唤醒它们。
2. 无名信号量
• 注:无名信号量和有名信号量都可以用作二进制信号量和计数信号量。
• api函数:
cpp
#include <semaphore.h>
/**
* @brief 在 sem 指向的地址初始化一个无名信号量。
*
* @param sem 信号量地址
* @param pshared 指明信号量是线程间共享还是进程间共享的
* 0: 信号量是线程间共享的,应该被置于所有线程均可见的地址(如,全局变量或在堆中动态分配的变量)
* 非 0: 信号量是进程间共享的,应该被置于共享内存区域,任何进程只要能访问共享内存区域,即可操作进程间共享的信号量
* @param value 信号量的初始值
* @return int 成功返回 0,失败返回-1,同时 errno 被设置以记录错误信息
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
/**
* @brief 销毁 sem 指向的无名信号量
*
* @param sem 无名信号量
* @return int 成功返回 0,失败返回-1,并设置 errno 指示错误原因
*/
int sem_destroy(sem_t *sem);
/**
* @brief 将 sem 指向的信号量加一,如果信号量从 0 变为 1,且其他进程或线程因信号量而阻塞,则阻塞的进程或线程会被唤醒并获取信号量,然后继续执行。POSIX 标准并未明确定义唤醒策略,具体唤醒的是哪个进程或线程取决于操作系统的调度策略。
*
* @param sem 信号量指针
* @return int 成功返回 0,失败则信号量的值未被修改,返回-1,并设置 errno 以指明错误原因
*/
int sem_post(sem_t *sem);
/**
* @brief 将 sem 指向的信号量减一。如果信号量的值大于 0,函数可以执行减一操作,然后立即返回,调用线程继续执行。如果当前信号量的值是 0,则调用阻塞直至信号量的值大于 0,或信号处理函数打断当前调用。
*
* @param sem 信号量指针
* @return int 成功返回 0,失败则信号量的值保持不变,返回-1,并设置 errno 以指明错误原因
*/
int sem_wait(sem_t *sem);
#include <time.h>
/**
* @brief 返回以秒为单位的 UNIX 时间戳
*
* @param tloc 记录时间的指针,如果不为 NULL,则当前的 UNIX 秒级时间戳也会存在tloc 指向的位置,否则不会存储。
* @return time_t 成功则返回以秒为单位的 UNIX 时间戳,失败则返回(time_t)-1
*/
time_t time(time_t *tloc);//time_t 本质上就是 long int 即 long 类型。
#include <stdlib.h>
/**
* @brief 返回一个 0-RAND_MAX 之间的伪随机数。当前机器环境下,RAND_MAX 为int 类型的最大值
*
* @return int 伪随机数
*/
int rand(void);
#include <stdlib.h>
/**
* @brief 将 seed 设置为 rand()生成随机数时使用的随机种子,如果没有设置随机种子,rand()会自动将 1 作为随机种子
*
* @param seed 随机种子
*/
void srand(unsigned int seed);
2.1 例子
• 作为二进制信号量用于线程间通信
• 二进制信号量的作用和互斥锁是相同的。
• 存在竞态条件的测试例子
cpp
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
int shard_num = 0;
void *plusOne(void *argv) {
int tmp = shard_num + 1;
shard_num = tmp;
}
int main() {
pthread_t tid[10000];
for (int i = 0; i < 10000; i++) {
pthread_create(tid + i, NULL, plusOne, NULL);
}
for (int i = 0; i < 10000; i++) {
pthread_join(tid[i], NULL);
}
printf("shard_num is %d\n", shard_num);
return 0;
}
• 这个代码,其结果可能出现不稳定的情况,就是最后的值可能少于10000。
• 这是因为 shared_num 的加一并不是原子操作,如果某个线程执行了 int tmp = shared_num+1,在它将 tmp 的值赋给 shared_num 之前,别的线程执行了第一步,那么加一操作就会被"吞掉" ,最终导致 10000 次加一的结果并未全部生效,因而shared_num 小于 10000。
• 加入无名信号量之后的例子
cpp
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
int shard_num = 0;
sem_t sem;
void *plusOne(void *argv) {
sem_wait(&sem);
int tmp = shard_num + 1;
shard_num = tmp;
sem_post(&sem);
}
int main() {
if(sem_init(&sem,0,1) != 0){
perror("sem_init");
return 1;
}
pthread_t tid[10000];
for (int i = 0; i < 10000; i++) {
pthread_create(tid + i, NULL, plusOne, NULL);
}
for (int i = 0; i < 10000; i++) {
pthread_join(tid[i], NULL);
}
printf("shard_num is %d\n", shard_num);
if(sem_destroy(&sem) != 0){
perror("sem_destroy");
return 1;
}
return 0;
}
• 其结果是稳定在10000
2.2 二值信号量用于进程间通信
• 例子,这部分代码存在竞态条件
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
char *shm_value_name = "unnamed_sem_shm_value";
// 创建内存共享对象
int value_fd = shm_open(shm_value_name, O_CREAT | O_RDWR, 0666);
// 调整内存共享对象的大小
ftruncate(value_fd, sizeof(int));
// 将内存共享对象映射到共享内存区域
int *value = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED, value_fd, 0);
// 初始化共享变量的值
*value = 0;
int pid = fork();
if (pid > 0) {
int tmp = *value + 1;
sleep(1);
*value = tmp;
// 等待子进程执行完毕
waitpid(pid, NULL, 0);
printf("this is father, child finished\n");
printf("the final value is %d\n", *value);
} else if (pid == 0) {
int tmp = *value + 1;
sleep(1);
*value = tmp;//同时赋值,相当于吞掉了一次+1操作。
} else {
perror("fork");
}
// 无论父子进程都应该解除共享内存的映射,并关闭共享对象的文件描述符
if (munmap(value, sizeof(int)) == -1)
{
perror("munmap value");
}
if (close(value_fd) == -1)
{
perror("close value");
}
// 如果调用时别的进程仍在使用共享对象,则等待所有进程释放资源后,才会销毁相关资源。
// shm_unlink 只能调用一次,这里在父进程中调用 shm_unlink
if (pid > 0)
{
if (shm_unlink(shm_value_name) == -1)
{
perror("father shm_unlink shm_value_name");
}
}
return 0;
}
//执行后我们发现 value 指向的位置值为 1。两次加一操作只生效一次,出现了竞态条件。
• 根据上述代码修改,引入信号量,解除竞态条件
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <semaphore.h>
int main() {
//创建信号量的共享内存 是 为了在不同进程间协调对共享资源的安全访问,防止资源竞争和数据不一致
//如果不创建信号量的共享内存,进程将无法共享信号量,导致无法同步。
char *shm_value_name = "unnamed_sem_shm_value";
//创建信号量 -> 使用共享内存创建
char *sem_value_name = "unnamed_sem_shm_sem";
// 创建内存共享对象
int value_fd = shm_open(shm_value_name, O_CREAT | O_RDWR, 0666);
int sem_fd = shm_open(sem_value_name, O_CREAT | O_RDWR, 0666);
// 调整内存共享对象的大小
ftruncate(value_fd, sizeof(int));
ftruncate(sem_fd, sizeof(sem_t));
// 将内存共享对象映射到共享内存区域
int *value = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED, value_fd, 0);
// 将共享内存的信号量映射到共享内存区域
sem_t *sem = mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE,
MAP_SHARED, sem_fd, 0);
// 初始化共享变量的值
*value = 0;
// 初始化信号量
sem_init(sem,1,1);
int pid = fork();
if (pid > 0) {
sem_wait(sem);
int tmp = *value + 1;
sleep(1);
*value = tmp;
sem_post(sem);
// 等待子进程执行完毕
waitpid(pid, NULL, 0);
printf("this is father, child finished\n");
printf("the final value is %d\n", *value);
} else if (pid == 0) {
sem_wait(sem);
int tmp = *value + 1;
sleep(1);
*value = tmp;//同时赋值,相当于吞掉了一次+1操作。
sem_post(sem);
} else {
perror("fork");
}
//父进程 执行到这里 子进程已经执行完毕 摧毁信号量
if(pid > 0){
if(sem_destroy(sem) == -1){
perror("sem_destroy");
}
}
// 无论父子进程都应该解除共享内存的映射,并关闭共享对象的文件描述符
if (munmap(sem, sizeof(sem_t)) == -1)
{
perror("munmap sem");
}
if (close(sem_fd) == -1)
{
perror("close sem");
}
if (munmap(value, sizeof(int)) == -1)
{
perror("munmap value");
}
if (close(value_fd) == -1)
{
perror("close value");
}
// 如果调用时别的进程仍在使用共享对象,则等待所有进程释放资源后,才会销毁相关资源。
// shm_unlink 只能调用一次,这里在父进程中调用 shm_unlink
if (pid > 0)
{
if (shm_unlink(shm_value_name) == -1)
{
perror("father shm_unlink shm_value_name");
}
if (shm_unlink(sem_value_name) == -1)
{
perror("father shm_unlink sem_value_name");
}
}
return 0;
}
//执行后发现,value 指向的位置值为 2,竞态条件已被解决。
• 注意事项,无名信号量用于进程间通信时,需要注意两点:
• sem_init()的第二个参数应设置为非零值,来告诉操作系统内核,这个信号量是用来进程间通信的,如果设置为 0,则一个进程通过 sem_post()释放的信号量无法被其它进程获取,会导致程序卡死。
• 信号量必须置于共享内存区域,以确保多个进程都可以访问 ,否则每个进程只会各自管理自己的信号量 ,后者并没有起到进程间通信的作用。
• 注:
• 上述代码,只给出了父子进程间通信的示例,但由于共享内存对象可以被任意进程访问,因此,无名信号量实际上可以用于任意进程间的通信,而不仅限于父子进程。在非父子进程通信时,共享资源的初始化和释放要格外注意,必须按照合理的顺序进行。
2.3 作为计数型信号量用于线程间通信,计数信号量主要是用于控制进程或线程执行顺序的。
cpp
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
//定义两个信号,一个表示缓冲区已经满了,一个表示缓冲区是空的状态。
sem_t *full;
sem_t *empty;
//缓冲区
int shard_num;
int get_num(){
srand(time(NULL));//设置随机数的种子
int num = rand();
return num;
}
void * produce(void * arg){
//写五次数据
for (size_t i = 0; i < 5; i++)
{
// 获取信号量
// 最开始 empty为1 可以减1 正常执行
// 下次再到这里 empty为0 不能减1 卡住
sem_wait(empty);//缓冲区为空的时候,写入数据
shard_num = get_num();
printf("生产者写入的数据为%d\n",shard_num);
sleep(1);
// 释放信号量
// 使full + 1 此时 消费者就能够获取信号量
sem_post(full);//缓冲区有数据了,让消费者读取
}
}
void * consume(void * arg){
//读取五次数据
for (size_t i = 0; i < 5; i++)
{
// 获取信号量
// 最开始 full为0 不能减1 卡住
sem_wait(full);//缓冲区有数据时候,读取数据
printf("消费者读取到的数据为%d\n",shard_num);
sleep(1);
// 释放信号量
// 消费者执行一轮 释放信号empty + 1 生产者就能获取信号量
sem_post(empty);//缓冲区为空了,让生产者写入
}
}
int main(int argc, char const *argv[])
{
//初始化两个信号量的指向
full = malloc(sizeof(sem_t));
empty = malloc(sizeof(sem_t));
//初始化两个信号量
sem_init(full,0,0);
sem_init(empty,0,1);
//创建生产者和消费者线程
pthread_t producer_id,consumer_t;
pthread_create(&producer_id,NULL,produce,NULL);
pthread_create(&consumer_t,NULL,consume,NULL);
//等待线程退出
pthread_join(producer_id,NULL);
pthread_join(consumer_t,NULL);
//销毁信号量
sem_destroy(full);
sem_destroy(empty);
return 0;
}
2.3.1 逻辑分析
• 在上述例程中,我们定义了两个信号量:full 和 empty,full 表示当前的缓冲区是否已满,empty 表示缓冲区是否已空。我们启动了一个消费者线程 consumer 和生产者线程 producer,前者用于从缓冲区读取数据(消费),后者用于向缓冲区写入数据(生产)。
• 生产者的逻辑:首先等待缓冲区为空,然后写入数据,最后通过 sem_post 告诉消费者,缓冲区已满,可以消费数据。
• 消费者的逻辑:首先等待缓冲区满,然后消费数据,最后通过 sem_post 告诉生产者,缓冲区已空,可以生产数据。
• 需要注意的是,初始时缓冲区是空的,因此,empty 信号量的初值应为 1,而 full 应为 0,二者都是用于线程间通信的,sem_init()函数的第二个参数都应该是 0。上述例程通过两个信号量控制生产者和消费者的执行顺序:
• 消费者必须在生产者发送数据后方可消费。
• 除首次发送外,生产者必须 等待消费者读取数据后 方可发送数据。
• 消费者必定在生产者发送数据后才会读取,两个线程的运行顺序被严格控制。
2.3.2 上面使用的信号量与二进制信号的区别和联系
• 上述案例中,信号量的取值仍在 0 和 1 之间变动,但这并不意味着本例中的也是二进制信号量。二进制信号量和计数信号量的划分更多地是从控制效果来说的 :二进制信号量起到了互斥锁的作用,当多个进程或线程访问共享资源时,确保同一时刻只有一个进程或线程进入了临界区,起到了"互斥"的作用;而计数信号量起到了"控制顺序"的作用,明确了"谁先执行"、"谁后执行"。很显然,本例是通过信号量控制了线程执行的先后顺序,属于计数信号量。
• 计数信号量和二进制信号量的划分不能只看信号量值的波动范围。
• 本例只展示了一个生产者和一个消费者在缓冲区为 1 时的协同工作,如果我们增加生产者和消费者的数量,信号量的取值范围自然就不再是 0 和 1 了。
2.4 作为计数信号量用于进程间通信
cpp
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
char * sem_name = "/unname_sem_shm";
int sem_fd = shm_open(sem_name,O_CREAT | O_RDWR,0666);
if(sem_fd == -1){
perror("shm_open");
return 1;
}
//truncate("/dev/shm/unname_sem_shm",sizeof(sem_t));//需要完整的路径
ftruncate(sem_fd,sizeof(sem_t));
//映射
sem_t * sem = mmap(NULL,sizeof(sem_t),PROT_READ | PROT_WRITE,MAP_SHARED,sem_fd,0);
//初始化信号量
sem_init(sem,1,0);
int pid = fork();
if(pid < 0){
perror("fork");
}else if(pid == 0){
sleep(1);
printf("this is son\n");
// 子进程释放信号量 sem+1 唤醒父进程
sem_post(sem);
}else {
// 父进程
// 子进程睡眠1s 保证父进程先执行
// sem_wait sem=0无法减1 卡住
sem_wait(sem);
printf("this is father\n");
sem_post(sem);
waitpid(pid,NULL,0);
}
if(pid > 0){
sem_destroy(sem);
}
// 父子进程都需要关闭文件描述符
if(munmap(sem,sizeof(sem_t)) == -1){
perror("munmap");
}
if(close(sem_fd) == -1){
perror("close");
}
if(pid > 0){
if(shm_unlink(sem_name) == -1){
perror("shm_unlink");
}
}
return 0;
}
3. 有名信号量
• 有名信号量的名称形如/somename,是一个以斜线(/)打头,\0 字符结尾的字符串, 打头的斜线之后可以有若干字符但不能再出现斜线,长度上限为 NAME_MAX-4(即 251)。不同的进程可以通过相同的信号量名称访问同一个信号量。
• 有名信号量通常用于进程间通信 ,这是因为线程间通信可以有更高效快捷的方式(全局变量等),不必"杀鸡用牛刀"。但要注意的是,正如上文提到的,可以用于进程间通信的方式通常也可以用于线程间通信。
• 有名信号量可以用于任意进程间的通信。
• 有名信号量的API函数
3.1 sem_open
cpp
/**
* @brief 创建或打开一个已存在的 POSIX 有名信号量。
*
* @param name 信号量的名称
* @param oflag 标记位,控制调用函数的行为。是一个或多个值或操作的结果。常用的是 O_CREAT。
* O_CREAT: 如果信号量不存在则创建,指定了这个标记,必须提供 mode 和 value
* @param mode 有名信号量在临时文件系统中对应文件的权限。需要注意的是,应确保每个需要访问当前有名信号量的进程都可以获得读写权限。
* @param value 信号量的初始值
* @return sem_t* 成功则返回创建的有名信号量的地址,失败则返回 SEM_FAILED,
同时设置 errno 以指出错误原因
*/
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
/**
* @brief 见四个参数的 sem_open()
*
* @param name 同上
* @param oflag 同上
* @return sem_t* 同上
*/
sem_t *sem_open(const char *name, int oflag);
3.2 sem_close
cpp
#include <semaphore.h>
/**
* @brief 关闭对于 sem 指向的有名信号量的引用,每个打开了有名信号量的进程在结束时都应该关闭引用
*
* @param sem 有名信号量指针
* @return int 成功返回 0,失败返回-1,并设置 errno 以指明错误原因
*/
int sem_close(sem_t *sem);
3.3 sem_unlink
cpp
/**
* @brief 移除内存中的有名信号量对象,/dev/shm 下的有名信号量文件会被清除。当没有任何进程引用该对象时才会执行清除操作。只应该执行一次。
*
* @param name 有名信号量的名称
* @return int 成功返回 0,失败返回-1,并设置 errno 以指明错误原因
*/
int sem_unlink(const char *name);
3.4 有名信号量用作二进制信号量
cpp
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
char *shm_value_name = "/named_sem_shm_value"; // 创建内存共享对象
char * sem_value_name = "/named_sem_value";
int value_fd = shm_open(shm_value_name, O_CREAT | O_RDWR, 0666);
ftruncate(value_fd, sizeof(int));
// 将内存共享对象映射到共享内存区域
int *value = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED, value_fd, 0);
sem_t *sem = sem_open(sem_value_name,O_CREAT,0666,1);
// 初始化共享变量的值
*value = 0;
int pid = fork();
if (pid > 0) {
sem_wait(sem);
int tmp = *value + 1;
sleep(1);
*value = tmp;
sem_post(sem);
// 等待子进程执行完毕
waitpid(pid, NULL, 0);
printf("this is father, child finished\n");
printf("the final value is %d\n", *value);
} else if (pid == 0) {
sem_wait(sem);
int tmp = *value + 1;
sleep(1);
*value = tmp;
sem_post(sem);
} else {
perror("fork");
}
//每个打开了有名信号量的进程在结束时都应该关闭引用
if(sem_close(sem) == -1){
perror("sem_close");
}
if (munmap(value, sizeof(int)) == -1)
{
perror("munmap value");
}
if (close(value_fd) == -1)
{
perror("close value");
}
// 如果调用时别的进程仍在使用共享对象,则等待所有进程释放资源后,才会销毁相关资源。
// shm_unlink 只能调用一次,这里在父进程中调用 shm_unlink
if (pid > 0)
{
if (shm_unlink(shm_value_name) == -1)
{
perror("father shm_unlink shm_value_name");
}
if (sem_unlink(sem_value_name) == -1)
{
perror("father sem_unlink sem_value_name");
}
}
return 0;
}
3.5 有名信号量用作计数型信号量
cpp
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
// char * sem_name = "/unname_sem_shm";
// int sem_fd = shm_open(sem_name,O_CREAT | O_RDWR,0666);
// if(sem_fd == -1){
// perror("shm_open");
// return 1;
// }
// //truncate("/dev/shm/unname_sem_shm",sizeof(sem_t));//需要完整的路径
// ftruncate(sem_fd,sizeof(sem_t));
// //映射
// sem_t * sem = mmap(NULL,sizeof(sem_t),PROT_READ | PROT_WRITE,MAP_SHARED,sem_fd,0);
// //初始化信号量
// sem_init(sem,1,0);
char * sem_name = "/name_sem";
sem_t *sem = sem_open(sem_name,O_CREAT,0666,0);
int pid = fork();
if(pid < 0){
perror("fork");
}else if(pid == 0){
sleep(1);
printf("this is son\n");
// 子进程释放信号量 sem+1 唤醒父进程
sem_post(sem);
}else {
// 父进程
// 子进程睡眠1s 保证父进程先执行
// sem_wait sem=0无法减1 卡住
sem_wait(sem);
printf("this is father\n");
sem_post(sem);
waitpid(pid,NULL,0);
}
//每个打开了有名信号量的进程在结束时都应该关闭引用
if(sem_close(sem) == -1){
perror("sem_close");
}
if(pid > 0){
if(sem_unlink(sem_name) == -1){
perror("sem_unlink");
}
}
return 0;
}
• 注:
• 创建的名为/named_sem 的信号量在/dev/shm 目录下。
• 创建的名为/named_sem 的信号量,将信号量名称去掉'/'后补充'sem.'前缀得到的字符串就是它在 tmpfs 中的对应文件名(sem.name_sem)。
4. 总结
• 可用于进程间通信的方式 通常都可以 用于线程间通信。
• 无名信号量和有名信号量均可用于进程间通信,有名信号量是通过唯一的信号量名称 在操作系统中唯一标识的。无名信号量用于进程间通信时必须将信号量存储在进程间可以共享的内存区域 ,作为内存地址直接在进程间共享。而内存区域的共享是通过内存共享对象的唯一名称来实现的。
• 无名信号量和有名信号量都可以作为二进制信号量和计数信号量使用。
• 二进制信号量和计数信号量的区别在于前者起到了互斥锁的作用 ,而后者起到了控制进程或线程执行顺序的作用。而不仅仅是信号量取值范围的差异。
• 信号量是用来协调进程或线程协同工作的,本身并不用于传输数据。
• 通常,从编码复杂度和效率的角度考虑,进程间通信使用有名信号量,线程间通信使用无名信号量。
• 信号量用于跨进程通信时,要格外注意共享资源的创建和释放顺序,避免资源泄露或在不恰当的时机释放资源从而导致未定义行为。
• 在生产环境的开发中,对于关键的步骤应当补充充分的错误处理,以便在错误发生时及时告警和响应。包括根据函数的返回值进行检查,结合使用 perror 或类似机制及时输出错误日志,以便快速排查和解决问题。此外,应确保适当释放资源以避免资源泄露。
5. System V的信号量API
cpp
1 #include <sys/sem.h>
2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
3 int semget(key_t key, int num_sems, int sem_flags);
4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
5 int semop(int semid, struct sembuf semoparray[], size_t numops);
6 // 控制信号量的相关信息
7 int semctl(int semid, int sem_num, int cmd, ...);
5.1 例子
cpp
#include "stdio.h"
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>
// int semget(key_t key, int nsems, int semflg);
//int semctl(int semid, int semnum, int cmd, ...);
// int semop(int semid, struct sembuf *sops, size_t nsops);
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
void pGetKey(int id){
struct sembuf set;
set.sem_num = 0; /* Operate on semaphore 0 */
set.sem_op = -1; /* Wait for value to equal 0 */
set.sem_flg = 0;//阻塞
semop(id,&set,1);
}
void vPutKey(int id){
struct sembuf set;
set.sem_num = 0; /* Operate on semaphore 0 */
set.sem_op = 1; /* Wait for value to equal 0 */
set.sem_flg = 0;
semop(id,&set,1);
}
int main(){
key_t key = ftok(".",2);//生成唯一的key
int semid;
semid = semget(key,1,IPC_CREAT|0666);//创建信号量集(1个信号量)
union semun initsem;//初始化信号量值为0
initsem.val = 0;
semctl(semid,0,SETVAL,initsem);//初始化信号量
int pid = fork();
if(pid <-1){
// pGetKey(semid);
printf("create fork fail\n");
}else if(pid == 0){
printf("this is child\n");
vPutKey(semid);
}else {
pGetKey(semid);
printf("this is father\n");
vPutKey(semid);
}
return 0;
}