15.信号灯(信号量)
Linux中的信号灯即信号量是一种用于进程间同步或互斥的机制,它主要用于控制对共享资源的访问。
在Linux系统中,信号灯作为一种进程间通信(IPC)的方式,与其他如管道、FIFO或共享内存等IPC方式不同,信号灯主要用于同步或互斥控制,以确保多个进程之间能够协调地访问共享资源。信号灯可以看作是内存中的一个标志,进程可以通过检查信号灯的状态来决定是否可以安全地访问某些共享资源。
要点
- 创建和操作:要在Linux中使用信号灯,首先需要创建一个信号灯,并指定其初始值。对于二值信号灯,这个初始值通常是1,表示资源一开始是可用的。
- 等待和释放:进程可以执行等待操作来测试信号灯的值,如果信号灯的值大于0,则进程可以继续执行并将信号灯的值减1;如果值为0,则进程必须等待直到信号灯的值变为正数。
- 共享资源控制:信号灯提供了一种机制,使得进程能够根据信号灯的状态来判断是否可以访问某些共享资源。这类似于互斥锁,确保了在任何时候只有一个进程可以访问临界区。
- 进程间通信:除了同步和互斥,信号灯还可以作为进程间通信的一种方式。它们存在于内核空间,与共享内存和消息队列一起构成了Linux中主要的IPC通信方式。
- 有名信号灯: 可以通过路径名在进程间共享,因此不同进程可以通过已知的路径名来访问同一个有名信号灯。 适用于需要在不同进程间进行同步的场景。
- 无名信号灯: 只能存在于内存中,因此使用无名信号灯的进程必须能够访问相同的内存区域。通过共享内存的方式创建,不依赖于文件系统中的路径名。适用于单进程内多线程间的同步或在已经映射相同内存内容的多个进程之间的同步。
- System V信号灯:Linux支持System V的信号灯,这是一种传统的信号灯实现,用于在同一系统内的进程间进行同步和互斥。
互斥和同步 是信号灯通常用于解决并发中的两个主要问题:
-
互斥:确保当一个进程使用共享资源时,其他进程不能同时访问该资源。例如,打印设备只能由一个进程使用,其他尝试访问打印设备的进程必须等待,直到当前进程完成打印任务。
-
同步:确保进程间的执行顺序符合特定的依赖关系。例如,一个进程生成数据,另一个进程消费这些数据,消费者进程需要等待生产者进程生成数据后才能继续执行。
PV操作
信号灯的工作原理基于PV操作,其中P操作用于请求资源(减少信号量的值),而V操作用于释放资源(增加信号量的值)。
-
P操作“proberen”(尝试):如果信号量的值为正,则将其减一,允许进程继续执行。如果信号量的值为0或负,则进程被阻塞,直到信号量的值变为正数。
-
V操作“verhogen”(释放):将信号量的值加一,如果有其他进程因等待该信号量而被阻塞,则其中一个进程会被唤醒。
POSIX是一组用于确保操作系统间可移植性的IEEE标准,主要针对Unix系统。POSIX标准为操作系统提供了一套共通的规则,使得软件开发更加高效,同时也让用户能够在不同的系统之间无缝地迁移和运行应用程序。
有名信号灯
用到的函数主要有:
sem_open
有名信号灯打开sem_close
有名信号灯关闭sem_unlink
有名信号灯的删除sem_wait
信号灯P操作,申请资源sem_post
信号灯V操作,释放资源
sem_open 函数
原型:
#include <fcntl.h> #include <sys/stat.h> #include <semaphore.h> sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
功能:创建或打开一个有名信号量(named semaphore)的函数
参数:
name
:信号量的名称,用于标识信号量。oflag
:打开选项,可以是以下值的组合:
O_CREAT
:如果信号量不存在,则创建一个新的信号量。O_EXCL
:与O_CREAT
一起使用,表示如果信号量已存在,则返回错误。O_RDWR
:允许对信号量进行读写操作。mode
:设置信号量的权限,通常设置为0644。value
:信号量的初始值。返回值:
- 成功时,返回一个指向信号量的指针。
- 失败时,返回SEM_FAILURE(通常是NULL)。
sem_close 函数
原型:
#include <semaphore.h> int sem_close(sem_t *sem);
功能:用于关闭一个信号量的函数
参数:
sem
:指向要关闭的信号量的指针返回值:
- 成功时,返回0。
- 失败时,返回-1,并设置errno。
sem_unlink 函数
原型:
#include <semaphore.h> int sem_unlink(const char *name);
功能:删除一个命名信号量
参数:
name
:要删除的信号量的名称返回值:
- 成功时,返回0
- 失败时,返回-1,并设置errno
sem_wait 函数
原型:
#include <semaphore.h> int sem_wait(sem_t *sem);
功能:”P操作“等待一个信号量。当信号量的值大于0时,该函数会将信号量的值减1,并立即返回。如果信号量的值为0,则该函数会阻塞当前线程,直到信号量的值大于0为止。
参数:
sem
:指向要等待的信号量的指针。返回值:
- 成功时,返回0。
- 失败时,返回-1,并设置errno。
sem_post 函数
原型:
#include <semaphore.h> int sem_post(sem_t *sem);
功能:”V操作“增加一个信号量的值。当信号量的值大于0时,如果有其他线程正在等待该信号量,则
sem_post
函数会唤醒其中一个等待的线程。如果没有线程在等待,那么信号量的值简单地增加,它与sem_wait
函数相对应.参数:
sem
:指向要操作的信号量的指针。返回值:
- 成功时,返回0。
- 失败时,返回-1,并设置errno。
示例-有名信号灯使用
使用信号量和共享内存实现的简单进程间通信(IPC)示例
一个简单的命令行界面,用户可以在命令行中输入字符串,并将其存储到共享内存中。通过信号量来实现进程间的同步,确保数据的读写操作不会发生冲突
tes_semw.c 写程序
#include <fcntl.h> #include <sys/stat.h> #include <semaphore.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/ipc.h> #include <sys/shm.h> #include <signal.h> void delsemfile(int sig) { sem_unlink("mysem_w"); // 删除名为"mysem_w"的信号量 exit(0); // 退出程序 } int main() { sem_t *sem_r,*sem_w; // 定义两个信号量指针,分别用于读和写操作 key_t key; // 定义键值,用于生成共享内存标识符 int shmid; // 定义共享内存标识符 char *shmaddr; // 定义共享内存地址指针 struct sigaction act; // 定义信号处理结构体 act.sa_handler = delsemfile; // 设置信号处理函数为delsemfile act.sa_flags = 0; // 设置信号处理标志为0 sigemptyset(&act.sa_mask); // 清空信号集 sigaction(SIGINT,&act,NULL); // 注册信号处理函数,当接收到SIGINT信号时调用delsemfile函数 key = ftok(".",100); // 生成键值,用于创建共享内存标识符 if(key<0) { perror("ftok"); // 如果生成失败,打印错误信息 return 0; } shmid = shmget(key,500,0666|IPC_CREAT); // 创建共享内存段,大小为500字节,权限为0666,如果不存在则创建 if(shmid<0) { perror("shmget"); // 如果创建失败,打印错误信息 return 0; } shmaddr = shmat(shmid,NULL,0); // 将共享内存段附加到进程的地址空间,并获取共享内存地址指针 sem_r = sem_open("mysem_r",O_CREAT|O_RDWR,0666,0); // 创建名为"mysem_r"的信号量,初始值为0 sem_w = sem_open("mysem_w",O_CREAT|O_RDWR,0666,1); // 创建名为"mysem_w"的信号量,初始值为1 while(1) { sem_wait(sem_w); // 等待名为"mysem_w"的信号量变为非零值 printf(">"); // 输出提示符">" fgets(shmaddr,500,stdin); // 从标准输入读取一行字符串,存储到共享内存中 sem_post(sem_r); // 增加名为"mysem_r"的信号量的值,表示数据已写入共享内存 } }
tes_semr.c 读程序
#include <fcntl.h> #include <sys/stat.h> #include <semaphore.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/ipc.h> #include <sys/shm.h> #include <signal.h> void delsemfile(int sig) { sem_unlink("mysem_r"); // 删除名为"mysem_r"的信号量 exit(0); // 退出程序 } int main(){ sem_t *sem_r,*sem_w; // 定义两个信号量指针,分别用于读和写操作 key_t key; // 定义键值,用于生成共享内存标识符 int shmid; // 定义共享内存标识符 char *shmaddr; // 定义共享内存地址指针 struct sigaction act; // 定义信号处理结构体 act.sa_handler = delsemfile; // 设置信号处理函数为delsemfile act.sa_flags = 0; // 设置信号处理标志为0 sigemptyset(&act.sa_mask); // 清空信号集 sigaction(SIGINT,&act,NULL); // 注册信号处理函数,当接收到SIGINT信号时调用delsemfile函数 key = ftok(".",100); // 生成键值,用于创建共享内存标识符 if(key<0) { perror("ftok"); // 如果生成失败,打印错误信息 return 0; } shmid = shmget(key,500,0666|IPC_CREAT); // 创建共享内存段,大小为500字节,权限为0666,如果不存在则创建 if(shmid<0) { perror("shmget"); // 如果创建失败,打印错误信息 return 0; } shmaddr = shmat(shmid,NULL,0); // 将共享内存段附加到进程的地址空间,并获取共享内存地址指针 sem_r = sem_open("mysem_r",O_CREAT|O_RDWR,0666,0); // 创建名为"mysem_r"的信号量,初始值为0 sem_w = sem_open("mysem_w",O_CREAT|O_RDWR,0666,1); // 创建名为"mysem_w"的信号量,初始值为1 while(1) { sem_wait(sem_r); // 等待名为"mysem_r"的信号量变为非零值 printf("%s\n",shmaddr); // 输出共享内存中的字符串 sem_post(sem_w); // 增加名为"mysem_w"的信号量的值,表示数据已读取完成 } }
运行效果如图,实现了两个进程通讯
可以通过命令
ls /dev/shm
查看信号量文件;在Linux系统中,/dev/shm目录是用于存放共享内存和信号量文件的特殊目录
ctrl + c
退出程序后,捕获到SIGINT
信号后执行delsemfile()
删除信号量文件
无名信号灯
用到的函数有:
sem_init
初始化无名信号量sem_destory
销毁无名信号量sem_wait
信号灯P操作,申请资源sem_post
信号灯V操作,释放资源
sem_init 函数
原型:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化一个无名信号量
参数:
sem
:指向要初始化的信号量的指针。pshared
:指定信号量的类型,如果为0,则表示该信号量是进程私有的;如果为非0值,则表示该信号量是进程间共享的。value
:指定信号量的初始值。返回值:
- 成功时,返回0;
- 失败时,返回-1,并设置
errno
。
sem_destory 函数
原型:
#include <semaphore.h> int sem_destroy(sem_t *sem);
功能:销毁一个无名信号量
参数:
sem
:指向要销毁的信号量的指针。返回值:
- 成功时,返回0;
- 失败时,返回-1,并设置
errno
。注意:只有当信号量的引用计数变为0时,才能安全地销毁它。如果还有其他线程或进程正在等待该信号量,那么销毁操作将失败,并且
errno
将被设置为EBUSY
。在这种情况下,需要确保所有使用该信号量的线程或进程都已经结束,然后再尝试销毁它。
示例-无名信号灯使用
使用信号量和共享内存实现的简单进程间通信(IPC)示例。主要功能是在一个进程中输入字符串,另一个进程中读取并打印这些字符串。
#include <fcntl.h> #include <sys/stat.h> #include <semaphore.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/ipc.h> #include <sys/shm.h> #include <signal.h> #include <pthread.h> sem_t sem_r,sem_w; // 定义两个信号量,用于读写同步 char *shmaddr; // 共享内存地址指针 void destroysem(int sig) // 信号处理函数,用于销毁信号量并退出程序 { sem_destroy(&sem_r); // 销毁读信号量 sem_destroy(&sem_w); // 销毁写信号量 exit(0); // 退出程序 } void *readmem(void *arg) // 读取共享内存中的字符串并打印的线程函数 { while(1) { sem_wait(&sem_r); // 等待读信号量 printf("%s\n",shmaddr); // 打印共享内存中的字符串 sem_post(&sem_w); // 释放写信号量 } } int main() { key_t key; // 键值 int shmid; // 共享内存标识符 struct sigaction act; // 信号处理结构体 act.sa_handler = destroysem; // 设置信号处理函数 act.sa_flags = 0; // 设置信号处理标志 sigemptyset(&act.sa_mask); // 清空信号集 sigaction(SIGINT,&act,NULL); // 注册信号处理函数 key = ftok(".",100); // 生成键值 if(key<0) { perror("ftok"); // 输出错误信息 return 0; } shmid = shmget(key,500,0666|IPC_CREAT); // 创建共享内存 if(shmid<0) { perror("shmget"); // 输出错误信息 return 0; } shmaddr = shmat(shmid,NULL,0); // 将共享内存映射到进程地址空间 sem_init(&sem_r,0,0); // 初始化读信号量,初始值为0 sem_init(&sem_w,0,1); // 初始化写信号量,初始值为1 pthread_t tid; // 线程标识符 pthread_create(&tid,NULL,readmem,NULL); // 创建线程,执行readmem函数 while(1) { sem_wait(&sem_w); // 等待写信号量 printf(">"); // 提示用户输入 fgets(shmaddr,500,stdin); // 从标准输入读取字符串并存储到共享内存中 sem_post(&sem_r); // 释放读信号量 } }
运行效果如图
在父进程中,它循环接收用户输入的数据并将其写入共享内存中。在子进程中,它循环读取共享内存中的数据并打印出来。
System V 信号灯
System V 信号灯是一种用于进程间同步和互斥的机制,它允许多个进程共享资源而不会产生冲突。
- 信号灯集合:System V 信号灯可以是一个或多个计数信号灯的集合。这意味着可以同时操作集合中的任意多个信号灯,以协调进程间的访问顺序。
- 信号灯操作:System V 信号灯的操作通常涉及三个步骤:初始化(创建)、P 操作(等待资源)和 V 操作(释放资源)。P 操作用于请求资源,如果资源不可用则进程将等待;V 操作用于释放资源,使得其他等待该资源的进程可以继续执行
- 避免死锁:通过申请多个资源时使用 System V 信号灯,可以减少死锁的风险。死锁是指两个或多个进程在互相等待对方释放资源时无法继续执行的状态。
System V 信号灯是一种重要的同步机制,它在多进程编程中扮演着关键角色,确保了资源的有序访问和数据的一致性。
用到的函数有:
semget
创建/打开信号灯semop
对信号灯集合中的信号量进行P - V操作semctl
信号灯集合的控制(初始化/删除)
semget 函数
原型:
#include <sys/sem.h> int semget(key_t key, int nsems, int semflg);
功能:获取一个信号量集的标识符
参数:
key
:是一个键值,用于唯一标识一个信号量集。通常使用ftok()
函数生成。nsems
:指定需要创建或获取的信号量数量。semflg
:设置信号量集的访问权限和创建标志。可以是以下值的组合:
IPC_CREAT
:如果信号量集不存在,则创建一个新的信号量集。IPC_EXCL
:与IPC_CREAT
一起使用,表示如果信号量集已经存在,则返回错误。0
:表示不设置任何特殊标志。返回值:
- 如果成功,返回信号量集的标识符(非负整数)。
- 如果失败,返回-1,并设置
errno
为相应的错误码。
semop 函数
原型:
#include <sys/sem.h> int semop(int semid, struct sembuf *sops, unsigned nsops);
功能:改变信号量的值,对信号灯集合中的信号量进行P - V操作
参数:
semid
:是一个信号量集的标识符,由semget()
函数返回。sops
:是一个指向struct sembuf
结构体数组的指针,该数组定义了要执行的操作。nsops
:指定sops
数组中操作的数量。
struct sembuf
结构体:struct sembuf { unsigned short sem_num; // 信号量编号 short sem_op; // 操作类型 short sem_flg; // 操作标志 };
各个字段的含义如下:
sem_num
:指定要操作的信号量的编号。如果设置为0,则表示对整个信号量集进行操作。sem_op
:指定要对信号量执行的操作类型。
- 当
sem_op
> 0,它表示进程释放控制的资源,即信号量的值将增加sem_op
的数量。- 当
sem_op
= 0,如果没有设置IPC_NOWAIT标志,调用进程将进入睡眠状态直到信号量的值为0;如果设置了该标志且信号量值不为0,则进程不会进入睡眠,而是直接返回EAGAIN。- 当
sem_op
< 0,它表示尝试获取资源使用权,信号量的值将增加sem_op
的绝对值。如果此时信号量的值小于或等于sem_op
的绝对值,操作将会阻塞,直到信号量的值大于或等于sem_op
的绝对值。sem_flg
:指定操作的标志。可以是以下值的组合:
IPC_NOWAIT
:非阻塞模式,如果无法立即执行操作,则立即返回。SEM_UNDO
:撤销之前的操作。0
:不设置任何特殊标志。返回值:
- 如果成功,返回0。
- 如果失败,返回-1,并设置
errno
为相应的错误码。
semctl 函数
原型:
#include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ...);
功能:控制信号量集
参数:
semid
:是一个信号量集的标识符,由semget()
函数返回。semnum
:指定要操作的信号量的编号。如果设置为0,则表示对整个信号量集进行操作。cmd
:指定要执行的命令。可以是以下值之一:
IPC_RMID
:删除信号量集。(常用)IPC_SET
:设置信号量集的属性。(常用)IPC_STAT
:获取信号量集的状态信息。(常用)SETVAL
:设置信号灯的值,需要用到第四个参数:共用体。(常用)IPC_INFO
:获取系统支持的信号量集的最大数量和当前使用的数量。SEM_STAT
:获取指定信号量的状态信息。GETALL
:获取所有信号量的值。GETNCNT
:获取等待某个信号量变为非零值的进程数。GETPID
:获取最后一个操作指定信号量的进程ID。GETVAL
:获取指定信号量的值。GETZCNT
:获取等待某个信号量变为零值的进程数。SETALL
:设置所有信号量的值。...
:根据cmd
的不同,可能需要传递额外的参数。返回值:
- 如果成功,返回相应的结果或状态信息。
- 如果失败,返回-1,并设置
errno
为相应的错误码
示例-System V信号灯使用
基于信号量和共享内存的进程间通信(IPC)示例。它创建了一个父子进程,并使用信号量和共享内存进行数据传递。
#include <semaphore.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/sem.h> #define SEM_READ 0 #define SEM_WRITE 1 //semctl函数用到的参数 union semun { int val; }; // P操作函数,用于对信号量进行减一操作 void Poperation(int semid,int semindex) { struct sembuf sbuf; sbuf.sem_num = semindex; sbuf.sem_op = -1; sbuf.sem_flg = 0; semop(semid,&sbuf,1); } // V操作函数,用于对信号量进行加一操作 void Voperation(int semid,int semindex) { struct sembuf sbuf; sbuf.sem_num = semindex; sbuf.sem_op = 1; sbuf.sem_flg = 0; semop(semid,&sbuf,1); } int main() { key_t key; // 定义键值key char *shmaddr; // 共享内存地址指针 int semid,shmid; // 信号量和共享内存的ID key = ftok(".",100); // 生成键值 if(key<0) { perror("ftok"); // 如果生成失败,输出错误信息 return 0; } semid = semget(key,2,IPC_CREAT |0666); // 创建信号量集合 if(semid<0) { perror("semget"); // 如果创建失败,输出错误信息 return 0; } shmid = shmget(key,500,IPC_CREAT |0666); // 创建共享内存 shmaddr = shmat(shmid,NULL,0); // 将共享内存映射到进程地址空间 union semun mysem; // 定义信号量联合体 mysem.val = 0; // 初始化读信号量的值为0 semctl(semid,SEM_READ,SETVAL,mysem); // 设置读信号量的值 mysem.val = 1; // 初始化写信号量的值为1 semctl(semid,SEM_WRITE,SETVAL,mysem); // 设置写信号量的值 pid_t pid; // 定义进程ID pid = fork(); // 创建子进程 if(pid<0) { perror("fork"); // 如果创建失败,输出错误信息 shmctl(shmid,IPC_RMID,NULL); // 删除共享内存 semctl(semid,0,IPC_RMID); // 删除信号量集合 exit(-1); }else if(pid == 0) { // 子进程循环读取共享内存中的数据并打印 while(1) { Poperation(semid,SEM_READ); // 对读信号量进行P操作 printf("%s\n",shmaddr); // 打印共享内存中的数据 Voperation(semid,SEM_WRITE); // 对写信号量进行V操作 } } else { // 父进程循环接收用户输入的数据并写入共享内存 while(1) { Poperation(semid,SEM_WRITE); // 对写信号量进行P操作 printf(">"); // 提示用户输入数据 fgets(shmaddr,32,stdin); // 从标准输入读取数据并写入共享内存 Voperation(semid,SEM_READ); // 对读信号量进行V操作 } } }
运行结果如图
在父进程中,它循环接收用户输入的数据并将其写入共享内存中。在子进程中,它循环读取共享内存中的数据并打印出来。