-
信号量
在创建子进程后,究竟是父进程还是子进程先运行这由操作系统调度策略决定,而如果要保证父子进程执行的先后顺序呢?我们可以使用sleep函数使得父子进行按照我们预想的顺序进行执行。但是要向我们那样使用sleep()函数,只能保证先执行子进程,但是不能保证子进程执行完后再执行父进程。所以如果我们想要子进程完全 执行完后再执行父进程,就可以利用信号量(Semaphore)来解决它们之间的同步问题。在前面我们讲过信号(Signal),其 实信号和信号量是两个不同的东西,它们虽然都可以实现同步和互斥,但是前者是使用信号处理器来进行的,而后者是使用P,V 操作来实现的。
-
PV操作的简单理解
在讲信号量之前,先了解两个概念同步和互斥:一条食品生产线上,假设A、B共同完成一个食品的包装任务,A负责将食品放 到盒子里,B和C负责将盒子打包。必须得是A先装食品B再打包吧,要是B不按规则先打包,那A还装啥,所以就需要一种机制方 法保证A先进行B再进行,“信号量”就是这种机制方法,AB之间的关系就是同步关系;再假设打包要用到刀子,而车间就有一 把刀子,这时候B和C就构成了互斥关系。 在多任务操作系统环境下,多个进程会同时运行,并且一些进程间可能会存在一定的 关联。多个进程可能为了完成同一个任务相互协作,这就形成了进程间的同步关系。而且在不同进程间,为了争夺有限的系统资 源(硬件或软件资源)会进入竞争状态,这就是进程间的互斥关系。
进程间的互斥关系与同步关系存在的根源在于临界资源。临界资源是在同一时刻只允许有限个(通常只有一个)进程可以访 问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器及其它外围设备等)和软件资源(共享代码段、共享 结构和变量等)。访问临界资源的代码叫做临界区,临界区本身也会称为临界资源。信号量是用来解决进程间的同步与互斥问题 的一种进程间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操 作(P/V操作)。其中,信号量对应于某一种资源,取一个非负的整形值。信号量值(常用sem_id表示)指的是当前可用的该资 源的数量,若等于0则意味着目前没有可用的资源。
-
PV操作定义
PV原子操作的具体定义如下:
-
P操作: 如果有可用的资源(信号量值>0),则此操作所在的进程占用一个资源(此时信号量值减1,进入临界区代码); 如果没有可用的资源(信号量值=0),则此操作所在的进程被阻塞直到系统将资源分配给该进程(进入等待队列,一直等 到资源轮到该进程)。
- V操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源 (即信号量值加1)
-
Linux下的信号量
在Linux系统中,使用信号量通常分为以下4个步骤:
-
创建信号量或获得在系统中已存在的信号量,此时需要调用 semget() 函数。不同进程通过使用同一个信号量键值来获得 同一个信号量。
-
初始化信号量,此时使用 semctl() 函数的SETVAL操作。当使用互斥信号量时,通常将信号量初始化为1。
-
进行信号量的PV操作,此时,调用 semop()函数。这一步是实现进程间的同步和互斥的核心工作部分。
-
如果不需要信号量,则从系统中删除它,此时使用semctl()函数的 IPC_RMID操作。需要注意的是,在程序中不应该出现 对已经被删除的信号量的操作。
-
ftok()函数简单介绍
共享内存,消息队列,信号量三种进程间通信方式都需要key_t类型的关键字ID值,就像我们需要一个唯一的身份证号来区分 一样,有时我们可以直接指定一个固定的整数值作为该ID值,但通常情况下会通过调用ftok()函数获取该值。在一般的UNIX实现 中,该函数是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。如指定文件的索引节点号为65538,换算成十六 进制为 0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。查询文件索引节点号 的方法是: ls -i filename
下面是ftok()函数的原型:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
该函数根据pathname指定的文件或目录的索引节点号和proj_id计算并返回一个key_t类型的ID值,如果失败则返回-1;
第一个参数pathname是一个系统中必须存在的文件或文件夹的路径,会使用该文件的索引节点;
第二个参数proj_id是用户指定的一个子序号,这个数字有的称之为project ID。它是一个8bit的整数,取值范围是1~255。
注意:如果要确保key值不变,要么确保ftok()的文件不被删除,要么不用ftok()指定一个固定的key值。
-
semget()函数创建或获取信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget( key_t key, int nsems, int semflg);
该函数用来创建一个信号集,或者获取已存在的信号集。成功返回信号量集的标识符,失败返回-1,errno被设置成以下的某个值:
第一个参数key:是所创建或打开信号量集的键值(ftok成果执行的返回值),不相关的进程可以通过它访问一个信号量,它代表程序可 能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget()函数并提供一个键,再由系统生成一个相应的信 号标识符(semget()函数的返回值),只有semget()函数才直接使用信号量键,所有其他的信号量函数使用由semget()函数返回的信 号量标识符。如果多个程序使用相同的key值,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),该联合不在任何系统头文件中定 义,需要我们自己在代码中定义:
union semun {
int val;
struct semid_ds* buf;
unsigned short* array;
struct seminfo* __buf;
};
-
semop()函数
#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;
short sem_op;
short sem_flg;
};
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.
-
编程示例:
我们将通过P、V操作来实现父子进程同步运行的例子。在初始化信号量时将信号量初始值设为0,如果父进程先运行的话 将会调用semaphore_p(semid) ,这时因为资源为0所以父进程会阻塞。而之后子进程运行时会执行semaphore_v(semid)将资 源+1,父进程之后就可以运行了。
-
示例代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define FTOK_PATH "/dev/null"
#define FTOK_PROJID 0x28
union semun
{
int val;
struct semid_ds * buf;
};
int semaphore_init(void);
int semaphore_p(int semid);
int semaphore_v(int semid);
void semaphore_term(int semid);
int main(int argc, char **argv)
{
int semid;
pid_t pid;
int i;
if( (semid=semaphore_init()) < 0)
{
printf("semaphore initial failure: %s\n", strerror(errno));
return -1;
}
if( (pid=fork()) < 0)
{
printf("fork() failure: %s\n", strerror(errno));
return -2;
}
else if( 0 == pid) //子进程
{
printf("Child process start running and do something here...\n");
sleep(3);
printf("Child process do something over...\n");
semaphore_v(semid); //v操作
sleep(1);
printf("Child process exit now\n");
exit(0);
}
printf("Parent process P operator wait child process over\n");
semaphore_p(semid); //p操作
printf("Perent process destroy semaphore and exit\n");
sleep(2);
printf("Child process exit.\n");
semaphore_term(semid); return 0;
}
int semaphore_init(void)
{
key_t key;
int semid;
union semun sem_union;
if( (key=ftok(FTOK_PATH, FTOK_PROJID)) < 0 )
{
printf("ftok() get IPC token failure: %s\n", strerror(errno));
return -1;
}
semid = semget(key, 1, IPC_CREAT|0644);
if( semid < 0)
{
printf("semget() get semid failure: %s\n", strerror(errno));
return -2;
}
sem_union.val = 0;
if( semctl(semid, 0, SETVAL, sem_union)<0 )
{
printf("semctl() set initial value failure: %s\n", strerror(errno));
return -3;
}
printf("Semaphore get key_t[0x%x] and semid[%d]\n", key, semid);
return semid; }
void semaphore_term(int semid)
{
union semun sem_union;
if( semctl(semid, 0, IPC_RMID, sem_union)<0 )
{
printf("semctl() delete semaphore ID failure: %s\n", strerror(errno));
}
return ;
}
int semaphore_p(int semid) //P操作
{
struct sembuf _sembuf;
_sembuf.sem_num = 0;
_sembuf.sem_op = -1;
_sembuf.sem_flg = SEM_UNDO;
if( semop(semid, &_sembuf, 1) < 0 )
{
printf("semop P operator failure: %s\n", strerror(errno));
return -1;
}
return 0;
}
int semaphore_v(int semid) //V操作
{
struct sembuf _sembuf;
_sembuf.sem_num = 0;
_sembuf.sem_op = 1;
_sembuf.sem_flg = SEM_UNDO;
if( semop(semid, &_sembuf, 1) < 0 )
{
printf("semop V operator failure: %s\n", strerror(errno));
return -1;
}
return 0;
}
-
运行结果
-
分析
通过运行结果我们可以知道,本例中是父进程先运行起来,为了使子进程先执行,父进程会通过P操作使自己阻塞,等待子进程的任务执行完之后,子进程会调用V操作解除父进程的阻塞,父进程就可以继续执行之后的操作。通过这种方式就可以实现子进程优先于父进程运行。