linux应用编程:进程间通信(四) 信号量 (进程间线程间同步)

一、概述

  • 信号量( semaphore)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段。
  • 信号量有三种类型:
    ① Posⅸ有名信号量:可用于进程或线程间的同步。
    ② Posix基于内存的信号量(无名信号量) 存放在内存区中,可用于进程或线程间的同步。常用于多线程间同步。
    ③ System V信号量(IPC机制):在内核中维护,可用于进程或线程间的同步。 常用于进程间同步。
  • 有名信号量通过文件系统中的路径名对应的文件名进行维护(信号量只是通过文件名进行标识,信号量的值并不存放到这个文件中,除非信号量存放在空间映射到这个文件上)。
  • 无名信号量通过用户空间内存进行维护,无名信号量要想在进程间通信,该内存必须为共享内存区。
  • System V信号量由内核进行维护。
  • 信号量可分为:
    ① 二值信号量( binary semaphore):其值或为0或为1的信号量。这与互斥锁类似,若资源被锁住则信号量值为0,若资源可用则信号量值为1。
    ② 计数信号量( counting semaphore):其值在0和某个限制值(对于Posiⅸ信号量,该值必须至少为32767)之间的信号量。信号量的值就是可用资源数。
  • Posix信号量为单个计数信号量,System V信号量为计数信号量集,偏集合的概念。
  • 由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
    P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
    V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
  • 除可以像互斥锁那样使用外,信号量还有一个互斥锁没有提供的特性:互斥锁必须总是由锁住它的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行。

二、System V信号量

2.1 简介

  • 常用于进程间同步,也可以用于线程间
  • 信号量的使用规则流程:
    (1)检测控制该资源的信号量。
    (2)若信号量的值为正,则进程可以使用该资源。然后将信号量值减 1,表示它使用了一个资源单位。此进程
    使用完共享资源后对应的信号量应该加 1。以便其他进程使用。
    (3)若对信号量进行减一时,信号量的值为 0,则进程进入阻塞休息状态,直至信号量值大于 0。进程被唤醒,
    返回第(1)步。
    为了正确地实现信号量,信号量值的测试及减 1 操作应当是原子操作(原子操作是不可分割的,在执行完毕
    不会被任何其它任务或事件中断)。为此信号量通常是在内核中实现的

在这里插入图片描述
在这里插入图片描述

  • 内核为每个信号量集合维护的 struct semid_ds
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct semid_ds
{
	struct ipc_perm	sem_perm;		/* permissions .. see ipc.h */
	__kernel_time_t	sem_otime;		/* last semop time */
	__kernel_time_t	sem_ctime;		/* last change time */
	struct sem	*sem_base;		/* ptr to first semaphore in array */
	struct sem_queue *sem_pending;		/* pending operations to be processed */
	struct sem_queue **sem_pending_last;	/* last pending operation */
	struct sem_undo	*undo;			/* undo requests on this array */
	unsigned short	sem_nsems;		/* no. of semaphores in array */
};

/* One semaphore structure for each semaphore in the system. */
struct sem
{
	int	semval;		/* current value *信号量的值*/
	int	sempid;		/* pid of last operation *最后一个操作信号量的进程号*/
	struct list_head sem_pending; /* pending single-sop operations */
};

2.2 接口

(1) semget

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h> 

/* @function  创建一个信号量集或访问一个已存在的信号量集。
 * @param[in] key 长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想让两个进程看到同一个信号集,只需设置key值不变就可以。
 * @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)

在这里插入图片描述
(2) semctl

/* @function 用来直接控制信号量信息
 * @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特有) 使用的缓存区
};

(3) semop

struct sembuf{
	short sem_num; //要操作的信号量在信号量集中的编号,第一个信号量的编号是 0。
	short sem_op; //sem_op 成员的值是信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是-1,也就是 p(减一)操作,它等待信号量变为可用;一个是+1,也就是 v(加一)操作,它发送信号通知信号量现在可用。
	short sem_flg; //通常设为:SEM_UNDO,程序结束,信号量为 semop 调用前的值。
};
/* @function 对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)

2.3 示例

参考:https://www.cnblogs.com/fangshenghui/p/4039946.html

  • 这位例程写得挺好的,借鉴参考一下
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<assert.h>
#include<time.h>
#include<unistd.h>
#include<sys/wait.h>
#define MAX_SEMAPHORE 10
#define FILE_NAME "test2.c"
 
union semun{
    int val ;
    struct semid_ds *buf ;
    unsigned short *array ;
    struct seminfo *_buf ;
}arg;

struct semid_ds sembuf;
 
int main()
{
    key_t key ;
    int semid ,ret,i;
    unsigned short buf[MAX_SEMAPHORE] ;
    struct sembuf sb[MAX_SEMAPHORE] ;
    pid_t pid ;
 
    pid = fork() ;
    if(pid < 0)
    {
        /* Create process Error! */
        fprintf(stderr,"Create Process Error!:%s\n",strerror(errno));
        exit(1) ;
    }


   if(pid > 0)
   {
        /* in parent process !*/
        key = ftok(FILE_NAME,'a') ;
        if(key == -1)
        {
             /* in parent process*/
             fprintf(stderr,"Error in ftok:%s!\n",strerror(errno));
             exit(1) ;
        }
 
        semid = semget(key,MAX_SEMAPHORE,IPC_CREAT|0666); //创建信号量集合
        if(semid == -1)
        {
            fprintf(stderr,"Error in semget:%s\n",strerror(errno));
            exit(1) ;
        }
        printf("Semaphore have been initialed successfully in parent process,ID is :%d\n",semid);
        sleep(2) ;
        printf("parent wake up....\n");
        /* 父进程在子进程得到semaphore的时候请求semaphore,此时父进程将阻塞直至子进程释放掉semaphore*/
        /* 此时父进程的阻塞是因为semaphore 1 不能申请,因而导致的进程阻塞*/
        for(i=0;i<MAX_SEMAPHORE;++i)
        {
            sb[i].sem_num = i ;
            sb[i].sem_op = -1 ; /*表示申请semaphore*/
            sb[i].sem_flg = 0 ;
        }
 
        printf("parent is asking for resource...\n");
        ret = semop(semid , sb ,10); //p()
        if(ret == 0)
        {
            printf("parent got the resource!\n");
        }
        /* 父进程等待子进程退出 */
        waitpid(pid,NULL,0);
        printf("parent exiting .. \n");
        exit(0) ;
    }
    else
    {
        /* in child process! */
        key = ftok(FILE_NAME,'a') ;
        if(key == -1)
        {
             /* in child process*/
             fprintf(stderr,"Error in ftok:%s!\n",strerror(errno));
             exit(1) ;
        }
 
        semid = semget(key,MAX_SEMAPHORE,IPC_CREAT|0666);
        if(semid == -1)
        {
              fprintf(stderr,"Error in semget:%s\n",strerror(errno));
              exit(1) ;
        }
        printf("Semaphore have been initialed successfully in child process,ID is:%d\n",semid);
 
        for(i=0;i<MAX_SEMAPHORE;++i)
        {
             /* Initial semaphore */
             buf[i] = i + 1;
        }
    
        arg.array = buf;
        ret = semctl(semid , 0, SETALL,arg);
        if(ret == -1)
        {
             fprintf(stderr,"Error in semctl in child:%s!\n",strerror(errno));
             exit(1) ;
        }
        printf("In child , Semaphore Initailed!\n");
 
        /* 子进程在初始化了semaphore之后,就申请获得semaphore*/
        for(i=0;i<MAX_SEMAPHORE;++i)
        {
            sb[i].sem_num = i ;
            sb[i].sem_op = -1 ;
            sb[i].sem_flg = 0 ;
        }

        ret = semop(semid , sb , 10);//信号量0被阻塞
        if( ret == -1 )
        {
            fprintf(stderr,"子进程申请semaphore失败:%s\n",strerror(errno));
            exit(1) ;
        }

        printf("child got semaphore,and start to sleep 3 seconds!\n");
        sleep(3) ;
        printf("child wake up .\n");
        for(i=0;i < MAX_SEMAPHORE;++i)
        {
            sb[i].sem_num = i ;
            sb[i].sem_op = +1 ;
            sb[i].sem_flg = 0 ;
        }

        printf("child start to release the resource...\n");
        ret = semop(semid, sb ,10) ;
        if(ret == -1)
        {
            fprintf(stderr,"子进程释放semaphore失败:%s\n",strerror(errno));
            exit(1) ;
        }
    
        ret = semctl(semid ,0 ,IPC_RMID);
        if(ret == -1)
        {
            fprintf(stderr,"semaphore删除失败:%s!\n",strerror(errno));
            exit(1) ;
        } 

        printf("child exiting successfully!\n");
        exit(0) ;
    }
    return 0;
}

三、Posix 信号量

3.1 简介

在这里插入图片描述

  • POSIX 信号量标准定义了有名信号量和无名信号量(基于内存的信号量)两种
    在这里插入图片描述
  • 常用于线程间的同步互斥操作

3.2 接口

  • 有名信号量创建与销毁
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>

/* @function 创建一个新的有名信号量或打开一个已存在的有名信号量。有名信号量总是既可用于线程间的同步,又可用于进程间的同步
 * @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);

/* @function 关闭信号量
 * @param[in] psem  sem_open的返回值
 * @return	成功 0, 失败 -1
 */
int sem_close(sem_t *psem);

//关闭一个信号量并没有将它从系统中刑除。
//这就是说, Posix有名信号量至少是随内核持续的:即使当前没有进程打开着某个信号量,它的值仍然保持。
//有名信号量使用sem_ unlink从系统中删除。

/* @function 销毁信号量
 * @param[in] name  信号量关联的文件
 * @return	成功 0, 失败 -1
 */
int sem_unlink(const char *name);
//每个信号量有一个引用计数器记录当前的打开次数(就像文件一样)
//sem_unlink类似于文件I/O的 unlink函数:当引用计数还是大于0时,name就能从文件系统中删除.
//然而其信号量的析构(不同于将它的名字从文件系统中删除)却要等到最后一个sem_close发生时为止
  • 无名信号量创建与销毁
#include <semaphore.h>

/* @function 初始化sem_t 变量的值。
 * @param[out] sem  sem_t变量的地址
 * @param[in] pshared 信号量的作用域
 * 			@arg: 为0时,信号量在同一进程的多线程间是共享的,用于线程间
 * 			@arg: 非0时,信号量用于多进程间,且分配的sem_t内存必须处于多进程的共享内存区
 * @param[in] value 信号量的初始值
 * @return	成功 信号量标识, 失败 -1
 */
int sem_init(sem_t *sem, int pshared, unsigned int value);

/* @function 销毁sem指向的信号量,并释放所占用的资源
 * @param[in] sem  指向信号量的指针
 * @return	成功 信号量标识, 失败 -1
 */
int sem_destroy(sem_t * sem);

– sem_open与sem_init的区别:

前者分配内存并初始化,然后返回一个指向该信号量的地址,后者只对已经存在的sem_t变量进行初始化。

/*基于文件的有名信号量初始化*/
sem_t *sem_p;
sem_p = sem_open("test.c", O_ CREAT|O_EXCL, 0666, 1);

/*基于内存的无名信号量初始化*/
sem_t sem;
sem_init(&sem, 0, 1); 
//尽量避免sem_init对同一个信号量多次初始化,因为对初始化过的信号量再进行初始化,结果是未知的
  • P 操作(申请资源或叫等待资源)
    – sem walt函数测试所指定信号量的值,如果该值大于0,那就将它减1并立即返回。如果该值等于0,调用线程就被投入睡眠中,直到该值变为大于0,这时再将它减1,函数随后返回。我们以前提到过,考虑到访问同一信号量的其他线程,“测试并减1”操作必须是原子的。
#include <semaphore.h>

/* @function 阻塞式等待信号量的值大于0时,并减1,线程执行后续操作,如果为0,使线程进入睡眠
 * @param[in] sem  指向信号量的指针或信号量的地址值
 * @return	成功 0, 失败 -1
 * @note 如果被某个信号中断, sem wait就可能过早地返回,所返回的错误为EINTR
 */
int sem_wait(sem_t *sem);

/* @function 非阻塞式等待信号量,没有等到不会进入睡眠,会返回一个EAGAIN错误 
 * @param[in] sem  指向信号量的指针或信号量的地址值
 * @return	成功 0, 失败 -1
 */
int sem_trywait(sem_t *sem);

/* @function 定时阻塞等待信号量,时间到达没有等到不会进入睡眠,会返回一个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 操作(释放资源)
/* @function 增加(解锁)sem指向的信号量,+1,唤醒正在等待该信号量值为正数的任一线程。
 * @param[in] sem  指向信号量的指针或信号量的地址值
 * @return	成功 0, 失败 -1
 */
sem_post( sem_t *psem ); // V 操作(加一)
  • 其他
/* @function 得到信号量当前值
 * @param[in] sem  指向信号量的指针或信号量的地址值
 * @param[out] sval 由sval 指向的整数中返回所指定信号量的当前值
 * @return	成功 0, 失败 -1
 * @note 如果该信号量当前已上锁,那么返回值或为0,或为某个负数,其绝对值就是等待该信号量解锁的线程数
 */
 int sem_getvalue(sem_t * sem, int * sval);

3.3 示例

  • 售票系统
#include<pthread.h>
#include<stdio.h>
#include <semaphore.h> 

int ticketcount = 10;
sem_t lock;

void *pthread1(void *args)
{   
    for(;;) {
        sem_wait(&lock); //因为要访问全局的共享变量 ticketcount,所以就要加锁
        if(ticketcount > 0) {
            printf("windows1 start sale ticket!the ticket is:%d\n",ticketcount);
            ticketcount --;//则卖出一张票
            sleep(3);
            printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
        } else {
            sem_post(&lock);
            break;
        }
        sem_post(&lock);
        sleep(1); //要放到锁的外面
    }
    pthread_exit(NULL);
}

void *pthread2(void *args)
{
    for(;;) {
        sem_wait(&lock); //因为要访问全局的共享变量 ticketcount,所以就要加锁
        if(ticketcount > 0) {//如果有票
            printf("windows2 start sale ticket!the ticket is:%d\n",ticketcount);
            ticketcount --;//则卖出一张票
            sleep(3);
            printf("sale ticket finish!,the last ticket is:%d\n",ticketcount);
        } else {
            sem_post(&lock);
            break;
        }
        sem_post(&lock);
        sleep(1); //要放到锁的外面
    }
    pthread_exit(NULL);
}
int main(int argc, char **argv)
{
    pthread_t pthid1,pthid2;

    sem_init(&lock,0,1); //信号灯值初始为 1,表示资源可用
    pthread_create(&pthid1,NULL,pthread1,NULL);
    pthread_create(&pthid2,NULL,pthread2,NULL);
    pthread_join(pthid1,NULL);
    pthread_join(pthid2,NULL);
    sem_destroy(&lock);

    exit(0);
}

四、总结

  • 我们现在看到了互斥锁、条件变量和信号量之间的更多差别:
    ① 首先,互斥锁必须总是由给它上锁的线程解锁。信号量没有这种限制:一个线程可以等待某个给定信号量(譬如说将该信号量的值由1减为0,这跟给该信号量上锁一样),而另一个线程可以挂出该信号量(如说将该信号量的值由0增为1,这跟给该信号量解锁一样)。
    ② 其次,既然每个信号量有一个与之关联的值,它由挂出操作加1,由等待操作减1,那么任何线程都可以挂出一个信号(譬如说将它的值由0增为1),即使当时没有线程在等待该信号量值变为正数也没有关系。然而,如果某个线程调用了 pthread_cond_ signa,不过当时没有任何线程阻塞在 othread_ cond wait调用中,那么发往相应条件变量的信号将丢失。
    ③ 最后,在各种各样的同步技巧(互斥锁、条件变量、读写锁、信号量)中,能够从信号处理程序中安全调用的唯一函数是 sem post。

  • 我们已看过的所有同步原语(互斥锁、条件变量、读写锁、信号量以及记录上锁)都有它们各自的位置。对于一个给定应用我们已有很多选择,因而需要了解各种原语之间的差别,还要从刚刚列出的比较中意识到的是,互斥锁是为上锁而优化的,条件变量是为等待而优化的,信号量既可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。

(1) 互斥锁必须总是由给它上锁的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行。这是我们的例子刚展示过的。
(2) 互斥锁要么被锁住,要么被解开(二值状态,类似于二值信号量)。
(3) 既然信号量有一个与之关联的状态(它的计数值),那么信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值