一、什么是POSIX信号量
POSIX信号量是一种用于线程之间同步和互斥的机制,它是由POSIX标准定义的一种IPC(进程间通信)机制,可以用于进程间或线程间的同步操作。POSIX信号量分成两种,POSIX匿名信号量和POSIX具体信号量。跟管道有点像,有匿名管道和具名管道。
二、特性
1、POSIX匿名信号量
(1)、通常用于线程间
(2)、只存在于内存,在文件系统中不可见
2、POSIX具名信号量
(1)、通常用在进程间
(2)、存在于文件系统 /dev/shm 中,可被不同进程操作
三、POSIX信号量使用步骤
1、POSIX匿名信号量
(1)、使用sem_init(),初始化匿名信号量
(2)、使用sem_wait()和sem_post()等,进行 P/V 操作
(3)、使用sem_destroy(),销毁匿名信号量
2、POSIX具名信号量
(1)、使用sem_open(),创建或打开具名信号量、
(2)、使用sem_wait()和sem_post()等,进行 P/V 操作
(3)、使用sem_close(),关闭具名信号量
(4)、使用sem_unlink(),彻底删除不再使用的信号量(可选)
四、相关的函数API接口
1、定义
// POSIX信号量是一种特性的变量 // 声明一个POSIX信号量s sem_t s;
2、初始化和销毁匿名信号量
// 初始化匿名信号量 int sem_init(sem_t *sem, int pshared, unsigned int value); // 接口说明 返回值:成功返回0,失败返回-1 参数sem:待初始化信号量指针 参数pshared:执行信号的作用范围 (1)0,作用于进程内的线程间 (2)非0,作用于进程间 参数value:信号量的初始值 // 销毁匿名信号量 int sem_destroy(sem_t *sem);
3、创建和打开具名信号量
// 打开 sem_t *sem_open(const char *name, int oflag); // 接口说明 返回值:成功返回信号量的地址,失败返回一个宏SEM_FAILED 参数name:具名信号量的路径 参数oflag:与文件打开的参数一样 // 创建 sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); // 接口说明 返回值:成功返回信号量的地址,失败返回一个宏SEM_FAILED 参数name:具名信号量的路径 参数oflag:与文件打开的参数一样 参数value:具名信号量初始值
4、P/V操作
// P操作,阻塞等待申请资源 int sem_wait(sem_t *sem); // P操作,非阻塞等待申请资源 int sem_trywait(sem_t *sem); // P操作,在一定时间内进行阻塞等待 int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // V操作,释放资源 int sem_post(sem_t *sem);
5、关闭具名信号量
POSIX具名信号量跟文件操作非常类似,打开之后会在内核需要对其维护,因此在不再需要的时候应该关闭
// 关闭具名信号量 int sem_close(sem_t *sem);
6、删除具名信号量
即使所有进程都关闭了信号量并且退出,具名信号量对应的文件是不会消失的,并且会保留所有 P/V 操作的值,如果不再需要这个文件本身,除了可以直接在文件系统中删除外,还可以使用以下接口删除
// 删除具名信号量 int sem_unlink(const char *name);
五、案例
1、使用POSIX匿名信号量实现线程间数据发送和接收,一条线程发送,一条线程接收
// POSIX匿名信号量的案例 #include <stdio.h> #include <pthread.h> #include <errno.h> #include <string.h> #include <semaphore.h> char data[100]; sem_t data_sem; // 声明一个POSIX匿名信号量data_sem pthread_once_t data_sem_once_init; // 函数单例变量,用来指定只初始化一次 pthread_once_t data_sem_once_destroy; // 函数单例变量,用来指定只销毁一次 // 初始化匿名信号量data_sem void data_sem_init(void) { // 定义data_sem, 指定用于线程间,初始值0,用来实现同步 int ret = sem_init(&data_sem, 0, 0); if(ret == -1) { perror("sem_init fail"); } } // 销毁匿名信号量data_sem void data_sem_destroy(void) { int ret = sem_destroy(&data_sem); if(ret == -1) { perror("sem_destroy fail"); } } // 线程1的例程函数,用来接收数据 void *recv_routine(void *arg) { printf("I am recv_routine, my tid = %ld\n", pthread_self()); // 设置线程分离 pthread_detach(pthread_self()); // 函数单例,本程序之后执行一次 pthread_once(&data_sem_once_init, data_sem_init); while(1) { // P操作,相当于给线程1发送信号 printf("wait data...\n"); sem_wait(&data_sem); printf("pthread1 read data: %s\n", data); memset(data, 0, sizeof(data)); } // 函数单例,本程序之后执行一次 pthread_once(&data_sem_once_destroy, data_sem_destroy); } // 线程2的例程函数,用来发送数据 void *send_routine(void *arg) { printf("I am send_routine, my tid = %ld\n", pthread_self()); // 设置线程分离 pthread_detach(pthread_self()); // 函数单例,本程序之后执行一次 pthread_once(&data_sem_once_init, data_sem_init); while(1) { printf("please input data:\n"); fgets(data, 100, stdin); // V操作,相当于给线程1发送信号 sem_post(&data_sem); printf("pthread2 send data\n"); } // 函数单例,本程序之后执行一次 pthread_once(&data_sem_once_destroy, data_sem_destroy); } int main(int argc, char *argv[]) { pthread_t tid1, tid2; // 创建线程1,用来接收数据 errno = pthread_create(&tid1, NULL, recv_routine, NULL); if(errno == 0) { printf("pthread create recv_routine success, tid = %ld\n", tid1); } else { perror("pthread create recv_routine fail\n"); } errno = pthread_create(&tid2, NULL, send_routine, NULL); if(errno == 0) { printf("pthread create send_routine success, tid = %ld\n", tid2); } else { perror("pthread create send_routine fail\n"); } // 一定要加这个,否则主函数直接退出,相当于进程退出,所有线程也退出 // 或者加上while(1)等让主函数不退出 pthread_exit(0); return 0; }
2、使用具名POSIX结合共享内存的方式,实现进程间互相收发数据
// POSIX具名信号量的案例 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <errno.h> #include <semaphore.h> #include <fcntl.h> #include <sys/stat.h> #define A 1 //#define B 1 // 编译第二版本时,请去掉前面的注释,同时注释A的宏定义 // 注意A进程的P信号量与B进程的V信号量相对应,所以要修改信号量序号的下标 #if A #define DATA_P_NAME "sem1" #define DATA_V_NAME "sem2" #define SPACE_P_NAME "sem3" #define SPACE_V_NAME "sem4" #elif B #define DATA_P_NAME "sem2" #define DATA_V_NAME "sem1" #define SPACE_P_NAME "sem4" #define SPACE_V_NAME "sem3" #endif #define SHM_KEY 0x01 #define SHM_SIZE 4096 // #define SEM_NAME "data_sem2" int sem_id = -1; // 映射的虚拟地址 char *shm_addr = NULL; // 共享内存初始化 int shm_init(void) { // 1、获取KEY值 key_t shm_key = ftok("./", SHM_KEY); if(shm_key == -1) { perror("ftok fail"); return -1; } // 2、指定共享内存,获取共享内存对象ID int shm_id = shmget(shm_key, SHM_SIZE, IPC_CREAT | 0666); if(shm_id == -1) { perror("shmget fail"); return -1; } // 3、映射共享内存 shm_addr = (char*)shmat(shm_id, NULL, 0); if(shm_addr == (void*)-1) { perror("shmat fail"); return -1; } } // 具名信号量初始化 sem_t *my_sem_init(const char *name, int value) { // 尝试打开具名信号量,并初始化为1 sem_t *sem = sem_open(name, O_EXCL| O_CREAT, 0666, value); // 如果具名信号量存在就不需要初始化,直接打开就行 if(sem == SEM_FAILED && errno == EEXIST) { sem = sem_open(name, O_CREAT); if(sem == SEM_FAILED) { perror("sem_open1 fail"); return NULL; } } else if(sem == SEM_FAILED) { perror("sem_open2 fail"); return NULL; } return sem; } int main(int argc, char *argv[]) { int ret = 0; ret = shm_init(); if(ret == -1) { return -1; } sem_t *Data_P = my_sem_init(DATA_P_NAME, 0); sem_t *Data_V = my_sem_init(DATA_V_NAME, 0); sem_t *Space_P = my_sem_init(SPACE_P_NAME, 1); sem_t *Space_V = my_sem_init(SPACE_V_NAME, 1); pid_t pid = fork(); // 父进程负责发送数据 if(pid > 0) { while(1) { // 申请空间,P操作 printf("wait Space_P...\n"); sem_wait(Space_P); printf("get Space_P\n"); printf("please input data: \n"); fgets(shm_addr, SHM_SIZE, stdin); // 释放数据,V操作 sem_post(Data_V); printf("set Data_V, send data success\n"); } } // 子进程负责接收数据 else if(pid == 0) { while(1) { // 申请数据,P操作 printf("wait Data_P...\n"); sem_wait(Data_P); printf("read Data: %s", shm_addr); memset(shm_addr, 0, SHM_SIZE); // 释放空间,V操作 sem_post(Space_V); printf("set Space_V\n"); } } else { perror("fork fail"); return -1; } return 0; }
注意:编译时编译两个版本,第一个版本直接编译,另外一个版本需要展开B的宏定义后再编译,跟前几篇的信号量组的案例一样。这个案例有点bug,每次需要重新把/dev/shm/下四个具名信号量都删除了才能重新运行中,目前努力修复中。
六、总结
POSIX信号量是一种用于线程之间同步和互斥的机制,当然也可以用于进程间通信。POSIX信号量有匿名信号量和具名信号量,跟管道有点像,两种信号量的使用步骤需要遵循一定的步骤,使用方法不一样。