信号量 - 信号灯
注意: 和信号没有任何关系
1> 信号灯是内核VAL资源
2> PV操作 P操作:申请资源 V:释放资源
3> IPC创建的是信号灯集
4> 信号灯不进行任何数据的传输,作为实现进程同步的工具
5> 信号灯的值不会主动归0
信号灯种类:
posix有名信号灯
posix基于内存的信号灯(无名信号灯)
System V信号灯(IPC对象) ***
信号灯的分类:
二值信号灯:值为0或1。与互斥锁类似,资源可用时值为1,不可用时值为0。
计数信号灯:值在0到n之间。用来统计资源,其值代表可用资源数。
(等待操作是等待信号灯的值变为大于0,然后将其减1;而释放操作则相反,用来唤醒等待资源的进程或者线程)
System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。
而Posix信号灯指是单个计数信号灯。
System V 信号灯由内核维护:主要函数 semget,semop,semctl
key semid owner perms nsems
信号灯集中信号灯的个数
信号量(Semaphore)
信号量(Semaphore),常被称为信号灯,它是不同进程间或一个给定进程内部不同线程间同步的机制。是 System V IPC 中用于进程同步的核心机制。它与信号(Signal)无关,而是通过计数器和原子操作(PV)协调多个进程对共享资源的访问。
信号量是一个计数器,用于控制多个进程对共享资源的访问。PV操作是核心,P操作申请资源,V操作释放资源;IPC创建的是信号灯集:System V IPC中的信号量不是单个而是以集合的形式存在。semget
创建或获取信号量集,nsems参数用于指定集合中的信号量数量。下面的代码中会创建信号量集,并初始化其中的信号量。
信号量不传输数据,只用于同步,它的用途是协调进程,而不是传递信息。信号量的值不会主动归零,所以程序员们需要进程显式地管理它们的值。所以代码中要时刻留意信号量的初始化和释放,需要正确管理信号量,避免资源泄漏。相关系统调用需要覆盖semget
、semctl
和semop
,尤其是semop
的sembuf
结构体,此处涉及执行PV操作。查看信号量状态的命令ipcs -s
输出查询。
内核中的数据结构,如struct sem_array
涉及对信号量的底层实现,以及信号量集如何在内核中管理。理解内核中信号量的工作原理是非常必要的环节。信号量的生命周期不会自动销毁,必须显式删除,否则会一直存在直到系统重启。这可能导致资源泄漏的问题。
- 内核管理的计数器(VAL 资源)
- 信号量是一个非负整数计数器,表示可用资源的数量。
- 通过 PV 操作 原子地修改计数器值:
- P 操作(Proberen,申请资源):计数器减 1,若计数器为 0 则阻塞。
- V 操作(Verhogen,释放资源):计数器加 1,唤醒阻塞的进程。
- 信号量集(Semaphore Set)
- System V 信号量以集合形式存在,一个信号量集可包含多个独立信号量。
- 例如:信号量集
semid
包含 3 个信号量,分别管理不同的资源。
- 不传输数据,仅同步
- 信号量仅用于协调进程的执行顺序,不传递任何数据。
- 常见应用:互斥锁(Mutex)、生产者-消费者模型。
- 生命周期管理
- 信号量由内核维护,需显式删除(
semctl(..., IPC_RMID)
),否则持续存在直至系统重启。
- 信号量由内核维护,需显式删除(
二、信号量的操作步骤
- 创建或获取信号量集
使用 semget()
创建或获取信号量集标识符:
#include <sys/sem.h>
key_t key = ftok("/tmp", 'A'); // 生成唯一键值
int semid = semget(key, nsems, IPC_CREAT | 0666);
key
:唯一标识信号量集的键值(通过ftok()
生成)。nsems
:信号量集中信号量的数量。semflg
:权限标志(如 IPC_CREAT | 0666)。
- 初始化信号量
使用 semctl()
初始化信号量的值:
union semun arg;
arg.val = 1; // 初始值设为1(互斥锁)
semctl(semid, 0, SETVAL, arg); // 初始化第0个信号量
- PV 操作
使用 semop()
执行原子操作:
struct sembuf op;
op.sem_num = 0; // 操作第0个信号量
op.sem_op = -1; // P操作(申请资源)
op.sem_flg = 0;
semop(semid, &op, 1); // 阻塞直到资源可用
// 临界区:访问共享资源
op.sem_op = 1; // V操作(释放资源)
semop(semid, &op, 1);
- 删除信号量集
semctl(semid, 0, IPC_RMID);
完整代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#if 0
unsigned short sem_num; /* semaphore number 信号灯的编号*/
short sem_op; /* semaphore operation PV操作的选择 */
// P: <0 V: >0
short sem_flg; /* operation flags 权限选择 */
0, 会阻塞进程 IPC_NOWAIT, 不会阻塞进程
#endif
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) */
};
int main()
{
//1- ftok()获得键值
key_t key = ftok("/",0x3);
if(key == -1)
{
perror("ftok");
return -1;
}
//利用semget获得/打开信号灯集
//int semget(key_t key, int nsems, int semflg);
int semid = semget(key,5,IPC_CREAT | 0666);
if(semid == -1)
{
perror("semget");
return -1;
}
printf("key = %#x,semid = %d\n",key,semid);
/* 查看信号灯的初始值 */
//int semctl(int semid, int semnum, int cmd, ...);
int val = semctl(semid,4,GETVAL); //得到编号为4的信号灯的当前值
if(val == -1)
{
perror("semctl");
return -1;
}
printf("val = %d\n",val);
/* 设置信号灯的值 */
union semun un; //更改信号灯值的联合体
un.val = 0; //将信号灯的值初始化为0
int ret = semctl(semid,4,SETVAL,un); //更改编号为4的信号灯的值
if(ret == -1)
{
perror("set error\n");
return -1;
}
#if 0
//执行V操作
//int semop(int semid, struct sembuf *sops, size_t nsops);
struct sembuf sem;
sem.sem_num = 4; //选择操作那个信号灯
sem.sem_op = 200; //执行V操作 sem_op > 0
sem.sem_flg = 0; //信号灯的值小于0阻塞进程
//semop(信号灯集的ID,PV操作的结构体,操作信号灯的个数)
if(semop(semid,&sem,1) == -1)
{
perror("sem_op");
return -1;
}
printf("-----------------------------\n");
//执行P操作
sem.sem_num = 4; //选择操作那个信号灯
sem.sem_op = -100; //执行P操作 sem_op < 0
sem.sem_flg = 0; //信号灯的值小于0阻塞进程
if(semop(semid,&sem,1) == -1)
{
perror("sem_op");
return -1;
}
//farsight 7433 0.0 0.0 4516 792 pts/1 S+ 16:45 0:00 ./a.out
printf("-----------------------------\n"); //不能输出这句话,原因是进程的信号灯的值小于0,导致进程阻塞
#endif
return 0;
}
三、信号量的状态查看
通过 ipcs -s
命令查看系统中的信号量集:
ipcs -s
------ Semaphore Arrays --------
key semid owner perms nsems
0x61005a3d 12345 user 666 3
key
:信号量集的唯一键值。semid
:信号量集的标识符。owner
:拥有者。perms
:权限(八进制格式,如 666 表示所有用户可读写)。nsems
:信号量集中的信号量数量。
四、信号量的底层原理
- 内核数据结构
struct sem_array
:内核为每个信号量集维护的结构体,包含权限、计数器值、等待队列等。- 计数器存储:每个信号量的值存储在
sem_base
数组中。
- 原子性保证
- PV 操作通过内核原子指令实现,确保计数器修改的不可分割性。
- 阻塞与唤醒
- P 操作阻塞:当计数器为 0 时,进程加入等待队列。
- V 操作唤醒:增加计数器后,唤醒等待队列中的进程。
完整代码:互斥锁实现
进程 A(申请资源):
#include <sys/sem.h>
#include <stdio.h>
int main() {
key_t key = ftok("/tmp", 'A');
int semid = semget(key, 1, IPC_CREAT | 0666);
union semun arg;
arg.val = 1;
semctl(semid, 0, SETVAL, arg); // 初始化为1
struct sembuf op = {0, -1, 0};
semop(semid, &op, 1); // P操作
printf("Process A entered critical section\n");
sleep(2);
printf("Process A exited critical section\n");
op.sem_op = 1;
semop(semid, &op, 1); // V操作
return 0;
}
进程 B(竞争资源):
#include <sys/sem.h>
#include <stdio.h>
int main() {
key_t key = ftok("/tmp", 'A');
int semid = semget(key, 1, 0666);
struct sembuf op = {0, -1, 0};
semop(semid, &op, 1); // P操作
printf("Process B entered critical section\n");
sleep(2);
printf("Process B exited critical section\n");
op.sem_op = 1;
semop(semid, &op, 1); // V操作
semctl(semid, 0, IPC_RMID); // 删除信号量集
return 0;
}
运行步骤:
- 编译并运行进程 A:
gcc process_a.c -o process_a
./process_a
- 在另一个终端运行进程 B:
gcc process_b.c -o process_b
./process_b
优点 | 缺点 |
---|---|
灵活支持复杂同步场景 | 编程复杂度高(需手动管理) |
可管理多个独立资源 | 性能低于用户态同步机制(如互斥锁) |
跨进程共享 | 需显式删除,否则资源泄漏 |
适用场景:
- 多进程互斥访问共享资源:如共享内存的读写保护。
- 生产者-消费者模型:协调生产者和消费者的执行顺序。
- 资源池管理:如数据库连接池的分配与回收。
综上。信号量是 System V IPC 中实现进程同步的核心工具,通过 PV 操作和计数器机制协调多进程对共享资源的访问。开发者需注意信号量的初始化、原子操作和生命周期管理,避免死锁和资源泄漏。不过在现代编程中更常用 POSIX 信号量或线程同步机制(后续的文章中将会提到这部分),System V 信号量在跨进程场景中仍具有很重要价值。
以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。
我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!