一、信号量
信号量是一个计数器,用于多个进程对共享资源区的访问。
- 当进程访问某个临界资源时,通过修改标志位,令其他进程无法访问该临界资源,此为信号量的锁机制
- 利用进程对临界资源访问休眠的机制,实现多个进程间同步或互斥的作用,例如:二元信号量PV操作
1.1 共享资源访问的流程
为获得对共享资源的访问,进程需要进行以下操作
- 测试访问该共享资源信号量
- 若该信号量值为正,则进程可以访问该共享资源,同时信号量值-1,表示进程正在进行访问
- 若该信号量值为0,则进程无法访问该共享资源,进入休眠状态,直至有进程退出对该共享资源的使用,信号量值大于0,返回步骤1
- 当进程不再使用共享资源时,信号量值+1,表示进程退出访问
为保证信号量的正确使用,对于信号量值的测试及加减操作是原子操作,不可被打断的操作,通常在内核中实现。
通常我们会使用二元信号量来控制在同一时刻没有或仅有一个进程对共享资源的访问,即信号量的PV操作。PV操作类似于我们过马路,当我们到达路口看见信号灯亮红灯时,我们就要停下等待直到信号灯亮起绿灯时才可以通过。
1. 2 信号量函数
1.2.1 semget函数-创建信号量集
/*
参数:
key:用户自定的信号量键值,一般使用ftok函数得到,用于标识一个信号量集,当我们要使两个进程关联同一个信号量集时,就要使用相同的键值
nsems:用于设置信号量集中信号量的个数,信号编号从0,1,2,...
semflg:权限标志位,当我们需要创建一个新的信号量集时,可以将IPC_CREAT跟其他权限标志按位与;设置为IPC_CREAT时,即使信号量集已存在也不会报错;设置为IPC_CREAT | IPC_EXCL时,则创建一个新的,且唯一的信号量集
返回值:成功信号量集的标识符,失败返回 -1
*/
int semget(key_t key, int nsems, int semflg);
1.2.2 semctl函数-控制信号量集
/*
参数:
semid:信号量集的标识符,用于指定控制的信号量集
semnum:信号量集的下标,用于标识控制信号量集中哪一信号量
cmd:指定操作类型,一般有IPC_RMID,删除信号量标识符;SETVAL,初始化信号量为一个已知的值;还有GETVAL返回信号量的值、SETALL初始化所有信号量等操作
返回值:成功返回 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 *array; /* 获取或设置信号量集中所有信号量的值 */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
1.2.3 semop函数-对信号量集操作
/*
参数:
semid:信号集的标识符
*sembuf:结构体数组指针,对信号量的操作
nsops:结构体数组的个数
返回值:成功返回 0,失败返回 -1
*/
int semop(int semid, struct sembuf *sops, size_t nsops);
struct sembuf{
unsigned short sem_num; /* 信号量在信号量集中的编号,第一个为0,以此类推 */
short sem_op; /* 信号量值的设置 */
short sem_flg; /* SEM_UNDO表示对信号量集的操作随进程退出结束,若进程退出前未关闭标识符释放共享资源,则内核代为释放 */
}
以上三个函数基本上构成对信号量的使用及关闭
1.2.4 PV操作流程
- 使用ftok函数获取一个唯一的key值
- 使用semget函数创建信号量集
- 使用semctl函数初始化信号量集
- 进程想要获取共享资源时,使用semop函数进行P操作(-1),若信号量值为0,则进程休眠等待,直到信号值大于0,执行第5步(其他情况在后文中讨论)
- 进程想要获取共享资源时,使用semop函数进行P操作(-1),若信号量值为1,则进程可对共享资源区进行操作,信号量值减1
- 进程不再使用共享资源时,使用semop函数进行V操作(+1),信号量值加1
- 最后一个使用信号量的进程必须明确地删除所使用的信号量集,来确保系统中不会有太多闲置的信号量集,从而导致无法创建新的信号量集,undo功能就是为了避免这种情况
代码示例:
信号量集的初始化程序
int semaphore_init(void)
{
key_t key=-1;
int semid=-1;
union semun semun_x;
key=ftok(PATH_NAME,PROJ_ID); //获取一个唯一的key值
if(key < 0)
{
printf("ftok failure:%s\n",strerror(errno));
return -1;
}
printf("key:%d\n",key);
semid=semget(key,1,IPC_CREAT | 0666); //不存在,则创建一个信号量集;存在,则引用该信号量集;该信号量集仅含一个信号量
if(semid < 0)
{
printf("semget failure:%s\n",strerror(errno));
return -2;
}
printf("semid:%d\n",semid);
semun_x.val=1; //设置信号量初始值为1
if( semctl(semid,0,SETVAL,semun_x) == -1) //对信号编号为0的信号量进行初始化操作
{
printf("semctl failure:%s\n",strerror(errno));
return -3;
}
return semid;
}
- 要使多个进程对同一个信号量集进行关联时,则应该使用相同的key值,同时引用现有集合的进程在semget函数中semflg标志位不可包含IPC_EXCL
- 信号的创建函数(semget)与控制函数(semctl)是相互独立的,这是信号量的一个缺点,这导致了不能原子地创建一个信号量集合,并对其进行初始化赋值
PV操作函数
//P操作
int semop_p(int semid)
{
struct sembuf sops;
sops.sem_num=0; //信号量集中信号编号为0的信号量
sops.sem_op=-1; //进程希望获取共享资源,对信号量值进行P操作(-1)
sops.sem_flg=SEM_UNDO; //当程序结束时,若没有释放共享空间,则内核代为释放
if(semop(semid,&sops,1))
{
printf("semop failure:%s\n",strerror(errno));
return -1;
}
return 0;
}
// V操作
int semop_v(int semid)
{
struct sembuf sops;
sops.sem_num=0;
sops.sem_op=1; //进程释放占用的共享资源,对信号量值进行V操作(+1)
sops.sem_flg=SEM_UNDO;
if(semop(semid,&sops,1))
{
printf("semop failure:%s\n",strerror(errno));
return -1;
}
return 0;
}
int semop_w(int semid)
{
struct sembuf sops;
sops.sem_num=0;
sops.sem_op=0; //表示调用进程等待信号量的值为0,函数返回
sops.sem_flg=SEM_UNDO;
if(semop(semid,&sops,1))
{
printf("semop failure:%s\n",strerror(errno));
return -1;
}
return 0;
}
对于sem_op取值有三种情况:
1.sem_op 为负值:这表示进程希望获取临界资源的使用权。
若当前可占用的资源数大于sem_op的绝对值,则用信号量值减去sem_op的绝对值,表示已被进程访问使用;
若当前可占用的资源数小于sem_op的绝对值,则有两种结果:标志位设为IPC_NOWAIT,则程序出错,返回一个error:EAGAIN;未设为IPC_NOWAIT,则程序进入休眠状态直到以下任一条件满足:(1)可占用资源数大于sem_op的绝对值,则用信号量值减去sem_op的绝对值,程序继续运行。(2)该信号量被删除,函数出错,返回EIDRM。(3)进程捕捉到信号,并前往执行信号处理函数,处理函数运行结束后,进程出错返回EINTR。(4)进程接收到默认操作为结束进程的信号,进程退出。
2.sem_op为正值:这表示进程结束对临界资源的使用,释放了占用的资源数,信号量的值会加上sem_op的值
3.sem_op为0:这表示进行希望等待到信号量的值为0
若信号量的值为0,则函数返回,程序向下执行
若信号量的值不为0,与sem_op为负值时,可占用资源数小于sem_op绝对值情况一致
注意:semop函数具有原子性,要么执行完操作数组中的所有操作,要么一个操作也不执行
释放共享资源
int semaphore_exit(int semid)
{
union semun semun_y;
if(semctl(semid,0,IPC_RMID,semun_y) < 0) //删除信号量集中信号编号为0的信号量
{
printf("semctl end failure:%s\n",strerror(errno));
return -1;
}
return 0;
}
信号量会一直保存在系统中,就算所有使用该信号量的进程结束也不会被摧毁,最后一个使用信号量的进程必须明确地删除所使用的信号量集
为避免在进程终止前未删除信号量集,UNIX推出了SEM_UNDO标志,这能确保无论在程序终止前是否释放资源,内核都会检验该信号量集分配的资源是否释放,对信号量集的操作是否删除,若未释放,则内核进行释放。