进程间通信机制(IPC)--信号量

信号量原理介绍

  在前面的几篇文章中有介绍到信号、管道、和命名socket,其中可以传送数据的通信方式是管道和命名socket,而信号和信号量虽然实现方式不同,但是他们都是不能够传递数据,通过信号的方式来执行指定操作。在进程间通信中我们可能会经常使用到fork()函数创建子进程,但是父进程和子进程的执行顺序却是随机的,前面为了解决这个问题,我们使用了简单的sleep()函数让父进程等待几秒后在执行,但这种方法是不可靠的,因此我们可以利用信号量来解决这个问题。在介绍信号量的概念前我们需要了解下面两个概念:
互斥:因为在很多情况下,进程使用的是局部变量,其分配的空间在进程的栈空间内,此时这个变量只能被该进程使用。而还有些变量需要在进程空间内共享,也就是多个进程访问的公共资源(临界资源)访问临界资源的代码区叫做临界区,而互斥就是为了保护每个时刻只有一个执行流进入临界区访问临界资源,对其进行保护。执行顺序一般是无序的。实现机制通过上锁:加锁 —> 执行临界区代码 —> 释放锁。
同步:同步关系中往往包含着互斥,其控制着进程与进程间的执行顺序,例如一个进程的运行要依赖另一个进程运行后的结果,因此他们间就需要同步来协调让后者先运行而前者等待。协调多个进程合作完成任务,执行顺序一般是有序的。
  进程间的互斥与同步的根源体现在临界资源中,临界资源在一个时刻只允许有限个(通常为一个)进程操作,其包括硬件资源(内存、处理器、存储器及外围设备)和软件资源(共享代码、共享结构和变量等)。访问临界资源的代码称为临界区。而信号量是一个特殊的变量,程序对其访问都是原子操作,只允许对其进行等待(P操作)和发送(V操作)的信息操作。

  • P(sv):如果sv值大于0,则减一,如果sv值为0,则挂起该进程的执行。
  • V(sv):如果有其他进程因等待sv被挂起则恢复运行,如果没有进程因等待挂起,则给sv加一。
      如果有两个进程共享信号量sv,其中一个进程执行P(sv)后,得到信号量且进入临界区,执行sv减一,第二个进程在第一个进程未执行完前被挂起,因为此时sv = 0,等待第一个进程离开临界区后会执行V(sv),使sv加一且释放该信号量。

信号量API介绍

ftok()获取关键字

  除信号量外在另外两种进程间通信(共享内存,消息队列)中都需要key_t类型的关键字ID值,用来唯一指定一个文件或文件夹路径,该ID值可以是我们自己定义的一个整型数值,更多的是通过调用ftok()来获取ID。在Unix中该函数会将文件的索引节点号取出在前面加上子序号作为key_t的返回值。如果要保证key值不变的话要保证ftok函数指定的文件不被删除,否则不用ftok函数指定一个固定的key值。例如文件索引节点号为13579换算为十六进制则为0x350b,你所指定的子序号为21,换算为16进制为0x15,则最后的key_t值为0x15350b,查询文件的索引节点号的命令是

ls -i filename  //file那么为该文件名
#include <sys/types.h> 
#include <sys/ipc.h>		//头文件

key_t ftok(const char *pathname, int proj_id);
//调用成功则返回key_t类型的ID,失败则返回-1
  • 第一个参数pathnema为一个指向系统存在的文件或文件路径的指针,会使用该文件的索引节点。
  • 第二个参数proj_id为用户自己定义的一个子序号,他是一个8bit的整数,取值范围为1~255。

semget()创建或获取信号量

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

int semget( key_t key, int nsems, int semflg);
//函数调用成功后会创建一个信号集或者获取已存在的信号集,返回信号量集的标识符,失败返回-1,并设置相应error
  • 第一个参数key是所创建或打开信号量集的键值,(ftok函数的返回值),他代表程序可能要使用的某个资源,程序对所有信号量的访问是间接的,先通过semget()函数并提供一个键,再有系统生成一个信号量标识符及该函数返回值,只有该函数才能直接使用key(信号量键),,其他函数使用的都是该函数返回的信号量标识符。
  • 第二个参数nsems为指定需要信号量的数目通常为1。
  • 第三个参数semflg是一组标志值,当想要创建一个新的信号量时,和值IPC_CREAT做按位或操作,设置了 IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

semctl()控制操作信号量集

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

int semctl(int semid, int semun, int cmd, ...);
//该函数用来初始或删除信号量集,成功调用后返回一个整数,失败返回-1
  • 第一个参数semid为semget函数返回的信号量键值。
  • 第二个参数semun表示操作信号在信号量集中的编号,第一个信号编号为0。
  • 第三个参数cmd为semid所指定的信号量集所要执行的命令可以为:SETVAL:设置信号量集中的一个单独的信号量的值,此时需要传入第四个参数;IPC_RMID:从系统中删除该信号量集合(删除);IPC_SEAT:对此集合取semid_ds 结构,并存放在由arg.buf指向的结构中(获取)。
  • 第四个参数为可选参数,其类型为semun的联合,该联合不会在头文件中定义需要我门自行定义,模板如下:
 union semun  
 {        
 		int val;        
 		struct semid_ds * buf;        
 		unsigned short * array;        
 		struct seminfo * __buf;   
 };

semop()完成信号量PV操作

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

int semop(int semid, struct sembuf * sops, unsigned nsops);
//该函数用来操作一个或一组信号,即PV操作,调用成功后返回0,失败后返回-1并设置errno。
  • 第一个参数semid同样为semget函数返回的信号量键值。
  • 第二个参数sops是一个指向信号量操作数组的指针,sembuf结构如下:
struct sembuf   
{
		unsigned short sem_num;  // 操作信号在信号集中的编号,第一个信号的编号是0,最后一个信号的编号是nsems-1。
 		short  sem_op;     
 		//操作为负(P操作)时, 其绝对值又大于信号的现有值,操作将会阻塞,直到信号值大于或等于 sem_op的绝对值。通常用于获取资源的使用权。 
 		//操作为正(V操作),该值会加到现有的信号内值上。通常用于释放所控制资源的使用权。                          
 		//操作为0时,如果后面的sem_flag没有设置IPC_NOWAIT,则调用该操作的进程或线程将暂时睡眠, 直到信号量的值为0;否则进程或线程会返回错误EAGAIN。
 		short  sem_flg; //可为下列两种方式:
 		//IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息
 		// SEM_UNDO:程序结束时(正常退出或异常终止),保证信号值会被重设为semop()调用前的值。避 免程序在异常情况下结束时未解锁锁定的资源,早成资源被永远锁定。造成死锁。
}
  • 第三个参数nsops为信号操作结构的数量,恒大于或等于1。

在linux下我们使用信号量通常是以下四个步骤

  • 创建或者获取系统中存在的信号量,semget()函数,不同进程使用同一个信号键值来获取一个相同信号量。
  • 初始化信号量,使用semctl()函数的SETVAL操作,当信号量为互斥关系时,将信号量初始化为1。
  • 对信号量进行PV操作,调用semop()函数来实现进程间的同步和互斥的核心部分工作。
  • 结束进程通讯时即删除不需要的信号量使用semctl()函数的IPC_RMID操作,删除信号量后,程序中不允许再对该信号量进行操作。

示例代码

  下面是一个没有加信号量的创建进程的代码:

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define DELAY_TIME 3
int main (int argc, char **argv)
{
        pid_t   pid;
        int     sem_id;
        pid = fork();
        if( pid < 0)
        {
                printf("fork() error: %s\n", strerror(errno));
                return -2;
        }
        else if( 0 == pid)
        {
                printf("child progress will wait for the parent process\n");
                sleep(DELAY_TIME);
                printf("Child process  PID[%d] running...\n", getpid());
        }
        else
        {
                printf("Parent process PID[%d] waiting...\n", getpid());
        }
        return 0;
}

  运行结果为:

panghu@Ubuntu-14:~$ ./fork 
Parent process PID[25414] waiting...
panghu@Ubuntu-14:~$ child progress will wait for the parent process
Child process  PID[25415] running...

  从上面可以看出由于子进程sleep一段时间后导致父进程先运行结束退出了,下面通过信号量的方式先让子进程运行,然后父进程等待子进程运行后再退出:

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define DELAY_TIME 3
union semun     //定义联合体
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};
//函数声明
int init_sem(int sem_id, int init_value);//信号量初始化
int del_sem(int sem_id);//从系统中删除函数
int sem_p(int sem_id);//P操作
int sem_v(int sem_id);//V操作
int main (int argc, char **argv)
{
        pid_t   pid;
        int     sem_id;
        //创建信号量
        sem_id = semget(ftok("fifo.c", 21), 1, 0664|IPC_CREAT);
        init_sem(sem_id, 0);
        pid = fork();
        if( pid < 0)
        {
                printf("fork() error: %s\n", strerror(errno));
                return -2;
        }
        else if( 0 == pid)//执行子进程
        {
                printf("child progress will wait for the parent process\n");
                sleep(DELAY_TIME);
                printf("Child process  PID[%d] running...\n", getpid());
                sem_v(sem_id);
        }
        else//执行父进程
        {
                sem_p(sem_id);
                printf("Parent process PID[%d] waiting...\n", getpid());
                sem_v(sem_id);
                del_sem(sem_id);
        }
        return 0;
}
//信号量初始化
int init_sem(int sem_id, int init_value)
{
    union semun sem_union;
    sem_union.val = init_value;  //init_value为初始值
    if(semctl(sem_id, 0, SETVAL, sem_union)==-1)
    {
        printf("Initialize semaphore");
        return -1;
    }
    return 0;
}
//删除信号量
int del_sem(int sem_id)
{
    union semun sem_union;
    if(semctl(sem_id, 0, IPC_RMID, sem_union)== -1)
    {
        printf("Delete semaphore");
        return -1;
    }
    }
//P操作
int sem_p(int sem_id)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;		//信号量编号,单个信号量应该为0
    sem_b.sem_op = -1;		//信号量操作,取-1值表示p操作
    sem_b.sem_flg = SEM_UNDO; //进程没释放信号量退出时系统自动释放
    if(semop(sem_id, &sem_b, 1)==-1)
    {
        printf("P operation");
        return -1;
    }
    return 0;
}
//V操作
int sem_v(int sem_id)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;		//信号量编号,单个信号量应该为0
    sem_b.sem_op = 1;		//信号量操作,取+1值表示v操作
    sem_b.sem_flg = SEM_UNDO;	//进程没释放信号量退出时系统自动释放
    if(semop(sem_id, &sem_b, 1)==-1)
    {
        printf("P operation");
        return -1;
    }
    return 0;
}

  运行结果如下

panghu@Ubuntu-14:~$ ./fork 
child progress will wait for the parent process
Child process  PID[25579] running...
Parent process PID[25578] waiting...

  通过信号量的方式保证了进程间的同步通信问题。

猜你喜欢

转载自blog.csdn.net/weixin_42647166/article/details/105117300