【进程与线程】信号量(Semaphore)

信号量 - 信号灯
	 注意: 和信号没有任何关系
        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参数用于指定集合中的信号量数量。下面的代码中会创建信号量集,并初始化其中的信号量。

信号量不传输数据,只用于同步,它的用途是协调进程,而不是传递信息。信号量的值不会主动归零,所以程序员们需要进程显式地管理它们的值。所以代码中要时刻留意信号量的初始化和释放,需要正确管理信号量,避免资源泄漏。相关系统调用需要覆盖semgetsemctlsemop,尤其是semopsembuf结构体,此处涉及执行PV操作。查看信号量状态的命令ipcs -s输出查询。

内核中的数据结构,如struct sem_array涉及对信号量的底层实现,以及信号量集如何在内核中管理。理解内核中信号量的工作原理是非常必要的环节。信号量的生命周期不会自动销毁,必须显式删除,否则会一直存在直到系统重启。这可能导致资源泄漏的问题。

  1. 内核管理的计数器(VAL 资源)
    • 信号量是一个非负整数计数器,表示可用资源的数量。
    • 通过 PV 操作 原子地修改计数器值:
    • P 操作(Proberen,申请资源):计数器减 1,若计数器为 0 则阻塞。
    • V 操作(Verhogen,释放资源):计数器加 1,唤醒阻塞的进程。
  2. 信号量集(Semaphore Set)
    • System V 信号量以集合形式存在,一个信号量集可包含多个独立信号量。
    • 例如:信号量集 semid 包含 3 个信号量,分别管理不同的资源。
  3. 不传输数据,仅同步
    • 信号量仅用于协调进程的执行顺序,不传递任何数据。
    • 常见应用:互斥锁(Mutex)、生产者-消费者模型。
  4. 生命周期管理
    • 信号量由内核维护,需显式删除(semctl(..., IPC_RMID)),否则持续存在直至系统重启。
      信号量

二、信号量的操作步骤

  1. 创建或获取信号量集

使用 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)。
  1. 初始化信号量

使用 semctl() 初始化信号量的值:

union semun arg;
arg.val = 1;  // 初始值设为1(互斥锁)
semctl(semid, 0, SETVAL, arg);  // 初始化第0个信号量
  1. 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);
  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:信号量集中的信号量数量。

四、信号量的底层原理

  1. 内核数据结构
    • struct sem_array:内核为每个信号量集维护的结构体,包含权限、计数器值、等待队列等。
    • 计数器存储:每个信号量的值存储在 sem_base 数组中。
  2. 原子性保证
    • PV 操作通过内核原子指令实现,确保计数器修改的不可分割性。
  3. 阻塞与唤醒
    • 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;
}

运行步骤:

  1. 编译并运行进程 A:
gcc process_a.c -o process_a
./process_a
  1. 在另一个终端运行进程 B:
gcc process_b.c -o process_b
./process_b

请添加图片描述

优点缺点
灵活支持复杂同步场景编程复杂度高(需手动管理)
可管理多个独立资源性能低于用户态同步机制(如互斥锁)
跨进程共享需显式删除,否则资源泄漏

适用场景:

  • 多进程互斥访问共享资源:如共享内存的读写保护。
  • 生产者-消费者模型:协调生产者和消费者的执行顺序。
  • 资源池管理:如数据库连接池的分配与回收。

综上。信号量是 System V IPC 中实现进程同步的核心工具,通过 PV 操作和计数器机制协调多进程对共享资源的访问。开发者需注意信号量的初始化、原子操作和生命周期管理,避免死锁和资源泄漏。不过在现代编程中更常用 POSIX 信号量线程同步机制(后续的文章中将会提到这部分),System V 信号量在跨进程场景中仍具有很重要价值。

以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。

我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫猫的小茶馆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值