Linux应用开发(6):Linux信号量:POSIX和System V信号量

1. 简述

        Linux的同步于互斥大致可以分为三个方式,分别是互斥锁,条件变量和信号量。他们针对不同的应用场景而被设计出来,其中互斥锁主要用于对共享资源的保护,条件变量用于控制操作的顺序,而信号量则用来控制对共享资源的访问数量。

        Linux信号量是一种用于进程间同步和互斥的低级同步机制。信号量可以在多个进程或线程之间协调对共享资源的访问。在Linux中,主要有两种信号量:POSIX信号量和System V信号量。它们各自有不同的特性和用途。

2. POSIX与System V信号量

        目前我们在Linux系统中常用的信号量主要有两种,分别是POSIX信号量和System V信号量。他们各自有对应的特点和应用场景。

POSIX:主要用于线程间的同步(无名信号量),也可以用在进程间(有名信号量)。

System V:常作为一种IPC机制,应用在进程间实现同步。

        综上,我们常说线程间实现同步和互斥,进程间通信都有信号量存在。

线程间同步和互斥手段:互斥锁、信号量、条件变量;

进程间通信(IPC):套接字、共享内存、消息队列、信号、信号量、管道;

3. POSIX信号量

        我们在前面已经讲过,POSIX信号量既可以用在线程间也可以用在进程间同步,但实际上我们使用的形式是不一样的。

POSIX信号量有两种:

无名信号量:基于内存的信号量,存储在内存中,一般用于线程间的同步;

有名信号量:基于系统文件的信号量,依赖于系统中的文件,一般用于进程间的同步,有名信号量一般用在关联进程间的通信,如通过fork实现的多进程

(1)无名信号量

        无名信号量寄生于内存中,用于线程间同步。具体的API如下所示。

        首先要引入相关头文件:

#include <fcntl.h>           /* For O_* constants */

#include <sys/stat.h>        /* For mode constants */

#include <semaphore.h>
初始化信号量

        需要注意的是,尽管sem_init的第二个参数为非0时,可以用于进程间同步,但我们一般不会这么用,因为不同的平台支持不一样。对于进程间同步还是建议使用有名信号量。

/*

*初始化sem_t 变量的值。

 * @param[out] sem  sem_t变量的地址

 * @param[in] pshared 信号量的作用域

 *            为0时,信号量在同一进程的多线程间是共享的,用于线程间

 *            非0时,信号量用于多进程间,且分配的sem_t内存必须处于多进程的共享内存区

 * @param[in] value 信号量的初始值

 * @return  成功 信号量标识, 失败 -1

 */

int sem_init(sem_t *sem, int pshared, unsigned int value);
销毁信号量
/*

*销毁sem指向的信号量,并释放所占用的资源

 * @param[in] sem  指向信号量的指针

 * @return  成功 信号量标识, 失败 -1

 */

int sem_destroy(sem_t * sem);

(2)有名信号量

创建有名信号量

/*

 * 创建或打开一个已存在的有名信号量。

 * 有名信号量既可用于线程间的同步, 也可用于进程间的同步

 * @param[in] name

 * @param[in] oflag 可以是0、O_CREAT或O_ CREAT|O_EXCL,如果指定了O_CREAT,需要第三、四个参数

 * @param[in] mode 读写权限 eg:0666

 * @param[in] value 指定信号量的个数

 * @return  成功 信号量标识, 失败 -1

 */

sem_t *sem_open(const char *name, int oflag);

sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

关闭有名信号量

        需要注意,关闭有名信号量以后,并没有把它从系统中清除。

/*

 * 关闭信号量

 * @param[in] psem  sem_open的返回值

 * @return  成功 0, 失败 -1

 */

int sem_close(sem_t *psem);

销毁信号量

/* @function 销毁信号量

 * @param[in] name  信号量关联的文件

 * @return  成功 0, 失败 -1

 */

int sem_unlink(const char *name);

(3)无名信号量和有名信号量的操作

        除了初始化和创建方式不一样,有名和无名信号量的操作是相同的。

P操作

        P操作就是对信号量执行-1操作。如果原值>0,那么-1操作以后就可以立即返回继续执行下面的操作。如果原值为0,那么当前线程将会进入阻塞休眠状态,等待其他线程+1。

#include <semaphore.h>


/*

 * 阻塞式等待信号量的值大于0时,并减1,线程执行后续操作,如果为0,使线程进入睡眠

 * @param[in] sem  指向信号量的指针或信号量的地址值

 * @return  成功 0, 失败 -1

 * @note 如果被某个信号中断, sem wait就可能过早地返回,所返回的错误为EINTR

 */

int sem_wait(sem_t *sem);

/*

 * 非阻塞式等待信号量,没有等到不会进入睡眠,会返回一个EAGAIN错误

 * @param[in] sem  指向信号量的指针或信号量的地址值

 * @return  成功 0, 失败 -1

 */

int sem_trywait(sem_t *sem);

/*

 * 定时阻塞等待信号量,时间到达没有等到不会进入睡眠,会返回一个ETIMEDOUT错误

 * @param[in] sem 指向信号量的指针或信号量的地址值

 * @param[in] abs_timeout  设置时间

 * @return  成功 0, 失败 -1

 */

struct timespec {

     time_t tv_sec;      /* Seconds */

     long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */

};

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

V操作

        V操作与P操作相反,执行+1操作,用于唤醒其他正在阻塞休眠的线程。

/*

 * +1,唤醒正在等待该信号量值为正数的任一线程。

 * @param[in] sem  指向信号量的指针或信号量的地址值

 * @return  成功 0, 失败 -1

 */

sem_post( sem_t *psem ); // V 操作(加一)

获取当前信号量的值

/*

 * 获取信号量当前值

 * @param[in] sem  指向信号量的指针或信号量的地址值

 * @param[out] sval 由sval 指向的整数中返回所指定信号量的当前值

 * @return  成功 0, 失败 -1

 * @note 如果该信号量当前已上锁,那么返回值或为0,或为某个负数,其绝对值就是等待该信号量解锁的线程数

 */

 int sem_getvalue(sem_t * sem, int * sval);

4. POSIX信号量例程

#include <stdio.h>  

#include <stdlib.h>  

#include <fcntl.h>  

#include <sys/stat.h>  

#include <semaphore.h>  

#include <unistd.h>  

#include <sys/wait.h>  

#include <sys/mman.h>  

 

/** 信号量名称 */ 

#define SEM_NAME "/task_sem"  

 

int main(int argc, char* argv[])

{

    sem_t *semaphore;  

    pid_t pid;  

 

    /** 打开或创建一个名为SEM_NAME的信号量,初始值为1. */

    semaphore = sem_open(SEM_NAME, O_CREAT, 0644, 1);  

    if (semaphore == SEM_FAILED) {  

        perror("sem_open");  

        exit(1);  

    }  

 

    pid = fork();  

    if (pid < 0) {  

        perror("fork");  

        exit(1);  

    } else if (pid == 0) {   ///< 子进程  

        printf("Child process is running...\n");  

        sem_wait(semaphore); ///< 等待信号量  

        printf("Child process has the semaphore and is performing work...\n");  

        sleep(3);            ///< 模拟工作  

        sem_post(semaphore); ///< 释放信号量  

        printf("Child process has released the semaphore and finished work.\n");  

    } else {                 ///< 父进程  

        printf("Parent process is running...\n");  

        sleep(1);            ///< 等待子进程先运行  

        sem_wait(semaphore); ///< 等待信号量  

        printf("Parent process has the semaphore and is performing work...\n");  

        sleep(3);            ///< 模拟工作  

        sem_post(semaphore); ///< 释放信号量  

        printf("Parent process has released the semaphore and finished work.\n");  

        wait(NULL);          ///< 等待子进程结束  

    }  

 

    sem_close(semaphore);    ///< 关闭信号量  

    sem_unlink(SEM_NAME);    ///< 删除信号量  

    return 0;  

}

5. System V信号量

        System V信号量作为一种IPC机制,常用在进程间的同步(理所当然的,也可以用在线程间的同步)。

(1)构建key_t

        需要注意,使用ftok生成key_t数据时,需要保证对应的路径名存在。如果要在不同进程进进行同步,那么该路径名不能够被删除和重建,否则会导致生成的key_t不相同,而造成进程间无法实现同步。

key_t ftok(const char *pathname, int proj_id);

(2)获得信号量

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>


/*

 * 创建一个信号量集或访问一个已存在的信号量集。

 * @param[in] key 长整型唯一非零值,通常情况下,该id值通过ftok函数得到,或者自行设定一个长整型值。

 * @param[in] nsems 指定集合中的信号量数,通常为1。如果我们不创建一个新的信号量集,而只是访问一个已存在的集合,那就可以把该参数指定为0。

 * @param[in] flags 一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。

 *                  设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。

 *                  而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。也可以是SEM_R和ISEM_A常值的组合。

 * @return  成功:信号量标识符,失败: -1

 */

int semget(key_t key, int nsems, int flags)

(3)控制信号量

/*

 * 控制信号量信息

 * @param[in] semid 由semget返回的信号量标识符

 * @param[in] semnum 为要进行操作的集合中信号量的编号,当要操作到成组的信号量时,

 *                  从 0 开始。一般取值为 0,表示这是第一个也是唯一的一个信号量

 * @param[in] cmd

 *      @arg IPC_RMID(立即删除信号集,唤醒所有被阻塞的进程)

 *      @arg GETVAL(根据semun 指定的编号返回相应信号的值, 此时该函数返回值就是你要获得的信号量的值,不再是 0 或-1)

 *      @arg SETVAL(根据 semun 指定的编号设定相应信号的值)

 *      @arg GETALL(获取所有信号量的值,此时第二个参数为 0,并且会将所有信号的值存入 semun.array 所指向的数组的各个元素中,此时需要用到第四个参数 union semun arg)

 *      @arg SETALL(将 semun.array 指向的数组的所有元素的值设定到信号量集中,此时第二个参数为 0,此时需要用到第四个参数 union semun arg)

 * @return 成功 0, 失败 -1

 */

int semctl(int semid, int semnum, int cmd, ...)


//第四个参数一般为是一个 union semun 类型(具体的需要由程序员自己定义),一般包含以下几个成员:

union semun {

    int val;  //使用的值

    struct semid_ds *buf;  //IPC_STAT、IPC_SET 使用的缓存区

    unsigned short *arry;  //GETALL,、SETALL 使用的数组

    struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区

};

(4)PV操作

struct sembuf{

    short sem_num; ///< 要操作的信号量在信号量集中的编号,第一个信号量的编号是 0。

    short sem_op;  ///< sem_op 成员的值是信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是-1,也就是 p(减一)操作,它等待信号量变为可用;一个是+1,也就是 v(加一)操作,它发送信号通知信号量现在可用。

    short sem_flg; ///< SEM_UNDO时当信号量小于0时会阻塞,设置为IPC_NOWAIT则不会阻塞。

};

/*

 * 对semid标识的信号量集中的一个信号量或多个信号量进行操作

 * @param[in] semid 由 semget 返回的信号量标识符

 * @param[in] sops 指向一个结构体数组的指针。可以指向单个或者多个结构体变量(即结构体数组)。

 * @param[in] nsops 为sops 指向的 sembuf 结构数组的长度

 * @return  成功 0, 失败 -1

 */

int semop(int semid, struct sembuf *sops, size_t nsops)

6. System V信号量例程

#include <stdio.h>  

#include <stdlib.h>  

#include <sys/types.h>  

#include <sys/ipc.h>  

#include <sys/sem.h>  

#include <sys/wait.h>  

#include <unistd.h>  



#define SEM_KEY 9999  ///< 信号量的键值,用于标识信号量集[也可以用ftok获得].

 

int main() {  

    int semid;  ///< 信号量集ID  

    struct sembuf op;  ///< 信号量操作结构  

    pid_t pid;  

 

    /** 创建信号量集,其中只有一个信号量. */

    semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);  

    if (semid == -1) {  

        perror("semget failed");  

        exit(1);  

    }  

 

    /** 初始化信号量的值为1. */

    if (semctl(semid, 0, SETVAL, 1) == -1) {  

        perror("semctl SETVAL failed");  

        exit(1);  

    }  

 

    pid = fork();  

    if (pid < 0) {  

        perror("fork failed");  

        exit(1);  

    } else if (pid == 0) {  ///< 子进程  

        printf("Child process is running...\n");  

 

        /** P操作:等待信号量. */

        op.sem_num = 0;  

        op.sem_op = -1;  ///< 减1操作  

        op.sem_flg = 0;  

        if (semop(semid, &op, 1) == -1) {  

            perror("semop -1 failed in child");  

            exit(1);  

        }  

 

        printf("Child process has the semaphore and is performing work...\n");  

        sleep(3);  ///< 模拟工作  

 

        /** V操作:释放信号量. */

        op.sem_op = 1;  ///< 加1操作  

        if (semop(semid, &op, 1) == -1) {  

            perror("semop +1 failed in child");  

            exit(1);  

        }  

 

        printf("Child process has released the semaphore and finished work.\n");  

    } else {  ///< 父进程  

        sleep(1);  ///< 让子进程先运行  

        printf("Parent process is running...\n");  

 

        /** P操作:等待信号量. */

        op.sem_num = 0;  

        op.sem_op = -1;  ///< 减1操作  

        op.sem_flg = 0;  

        if (semop(semid, &op, 1) == -1) {  

            perror("semop -1 failed in parent");  

            exit(1);  

        }  

 

        printf("Parent process has the semaphore and is performing work...\n");  

        sleep(3);  ///< 模拟工作  

 

        /** V操作:释放信号量. */

        op.sem_op = 1;  ///< 加1操作  

        if (semop(semid, &op, 1) == -1) {  

            perror("semop +1 failed in parent");  

            exit(1);  

        }  

 

        printf("Parent process has released the semaphore and finished work.\n");  

 

        wait(NULL);  ///< 等待子进程结束  

 

        /** 删除信号量集. */

        semctl(semid, 0, IPC_RMID);  

    }  

 

    return 0;  

}

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值