linux 内核对信号量具体实现
信号量有两种实现:传统的 System V 信号量和新的 POSIX 信号量.
使用区别
1、XSI system V 的信号量是信号量集,可以包括多个信号灯(有个数组),每个操作可以同时操作多个信号灯,posix 是单个信号灯,POSIX 有名信号灯支持进程间通信,无名信号灯放在共享内存中时可以用于进程间通信。
2、POSIX 信号量在有些平台并没有被实现,比如:SUSE8,而 SYSTEM V 大多数LINUX/UNIX 都已经实现。两者都可以用于进程和线程间通信。但一般来说,system v 信号量用于进程间同步、有名信号灯既可用于线程间的同步,又可以用于进程间的同步、posix 无名用于同一个进程的不同线程间,如果无名信号量要用于进程间同步,信号量要放在共享内存中。
3、POSIX 有两种类型的信号量,有名信号量和无名信号量。有名信号量像 system v信号量一样由一个名字标识。
4、POSIX 通过 sem_open 单一的调用就完成了信号量的创建、初始化和权限的设置,而 system v 要两步。也就是说 posix 信号是多线程,多进程安全的,而 system v不是,可能会出现问题。
5、system V 信号量通过一个 int 类型的值来标识自己(类似于调用 open()返回的 fd),而 sem_open 函数返回 sem_t 类型(长整形)作为 posix 信号量的标识值。
6、对于 System V 信号量你可以控制每次自增或是自减的信号量计数,而在 Posix 里面,信号量计数每次只能自增或是自减 1。
7、Posix 无名信号量提供一种非常驻的信号量机制。
8、相关进程: 如果进程是从一已经存在的进程创建,并最终操作这个创建进程的资源,那么这些进程被称为相关的。
9.它们所提供的函数很容易被区分:对于所有 System V 信号量函数,在它们的名字里面没有下划线。例如,应该是 semget()而不是 sem_get()。然而,所有的的 POSIX 信号量函数都有一个下划线。
注意事项
1、Posix 有名信号灯的值是随内核持续的。也就是说,一个进程创建了一个信号灯,这个进程结束后,这个信号灯还存在,并且信号灯的值也不会改变。当持有某个信号灯锁的进程没有释放它就终止时,内核并不给该信号灯解锁
2、posix 有名信号灯是通过内核持续的,一个进程创建一个信号灯,另外的进程可以通过该信号灯的外部名(创建信号灯使用的文件名)来访问它。posix 基于内存的无名信号灯的持续性却是不定的,如果基于内存的信号灯是由单个进程内的各个线程共享的,那么该信号灯就是随进程持续的,当该进程终止时它也会消失。如果某个基于内存的信号灯是在不同进程间同步的,该信号灯必须存放在共享内存区中,这要只要该共享内存区存在,该信号灯就存在。
system V 信号量实现方式
system V 信号量使用步骤
1. 先向内核申请一个 IPC key(钥匙,"许可证")
2. 打开或者创建一个 IPC 的“对象(sem)”
3. 控制操作(设置/获取信号量集中的值)
4. 操作信息量 P/V 操作
key 创建函数(ftok)
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
函数原型:
key_t ftok(const char *pathname, int proj_id);
作用:
把一个路径名和一个整数值转换成一个特定的 key,只要文件名和整数值相同,产生的 key 就会相同
参数含义:
pathname:一个存在的路径名,ftok 获取这个文件的 inode 编号
proj_id:一个整数值(非 0)
返回值:
成功返回一个 IPC 的 key(int)
失败返回-1,同时 errno 被设置
备注:
如果两个进程要通过 System V 的 IPC 对象通信,那么他们引用的 IPC 对象必须是同一个,使用的 key 必须相同,使用相同的 key 就会打开同一个 IPC 对象
使用示例:
//向内核申请一个 IPC key(钥匙,"许可证") ftok
#define PATHNAME "/home/china"
#define PROJ_ID 20207
key_t key = ftok(PATHNAME,PROJ_ID);
if(-1 == key)
{
perror("ftok error");
exit(-1);
}
打开或者创建一个 IPC 的“对象(sem)”
对于每一个信号量集,内核维护了一个如下的结构体(在/usr/include/linux/sem.h 文件中)
struct semid_ds {
struct ipc_perm sem_perm; /* 权限 */
__kernel_time_t sem_otime; /* 最后操作时间 */
__kernel_time_t sem_ctime; /* 最后修改时间(属性) */
struct sem *sem_base; /* 指向信号量数组中的第一个成员 */
struct sem_queue *sem_pending; /* 待处理的挂起操作 */
struct sem_queue **sem_pending_last; /* 最后等待操作 */
struct sem_undo *undo; /* 撤销此数组上的请求*/
unsigned short sem_nsems; /* 信号量数组中有多少个信号量 */
};
其中 struct sem 就是标识一个 system V 的信号量,它的结构如下
struct sem
{
unsigned short semval; //信号量的值,其值在 0 和某个限制值之间的信号量,信号量的值就是可用资源数
unsigned short semzcnt; // 等待它变为 0 的进程数量
unsigned short semncnt; //wait 其增长的进程数
//等待该信号量值绝对值大于当前值的进程数量
//也就是需求资源数大于系统可用资源数的进程数量
pid_t sempid; //最后一次 P/V 操作的进程的 ID
};
semget 函数就可以打开或者创建一个 system V 的计数信号量集
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semget(key_t key, int nsems, int semflg);
作用:
打开或者创建一个 system V 的计数信号量集
参数含义:
key: system V IPC 对象的 key(一般由 ftok 函数返回)
nsems: 计数信号量集中有多少个信号量,如果我们的操作不是创建一个信号量集, 而是打开一个信号量集,次参数可以指定为 0,一旦创建完一个信号量集,其信号量的个数就不能改变了, 一般来说,你需要多少个保护的对象,就需要多少个信号量
semflg:标志位,如果是创建操作的话则为 IPC_CREAT | 权限位,如果是打开操作的话填 0
返回值:
成功返回一个 system V 信号量集的 id
失败返回-1,同时 errno 被设置
示例:
//利用 key 创建或者打开一个 IPC 对象 --->信号量
int sem_id = semget(key_sem,1, IPC_CREAT | 0664);
if(sem_id == -1)
{
perror("semget error");
return -1;
}
信号量集的控制操作-semctl
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semctl(int semid, int semnum, int cmd, ...);
作用:
根据命令号 cmd 对 semid 信号量集中的第 semnum 个信号量执行控制操作
参数含义:
semid: 要操作的信号量集的 id;
semnum: 要操作信号量集中的哪一个信号量,就是信号量数组的下标
cmd: 命令号
返回值:
失败时,semctl()返回-1,errno 表示错误。
否则,系统调用将根据 cmd 返回一个非负值。
常用命令号:
GETALL:获取信号量集中所有信号量的值
当 cmd 为 GETALL 时,第四个参数应该是一个 ushort(unsigned short) vals[],用来保存每一个信号量中值,例如:
ushort vals[10] = {0}; //用来保存 10 个信号量中的值semctl(semid,0, GETALL,vals); //获取所有信号量中的值
SETALL:设置信号量集中所有信号量的值
当 cmd 为 SETALL 时,第四个参数应该是一个 ushort(unsigned short) vals[],用来设置每一个信号量中值,例如:
ushort vals[10] = {1,1,1,1,1,1,1,1,1,1};semctl(semid,0, SETALL,vals); //把 vals 中指定的值设置到信号量集中
GETVAL:获取信号量集中指定信号量的值
第四个参数不需要,函数的返回值就表示那个(semnum)信号量的值,例如:
int val = semctl(semid,1,GETVAL); //获取第 1 个信号量的值
SETVAL:设置信号量集中指定信号量的值
第四个参数应该是一个 int 值,表示你要设置的信号量的目标值,例如:
int val = semctl(semid,1,GETVAL,1); //设置第 1 个信号量的值为 1
IPC_RMID:删除一个信号量集对象
第四个参数不需要,如果删除一个正在被使用的信号量结果是未定义的,例如:
semctl(semid,0,IPC_RMID); //删除第 0 个信号量
IPC_STAT:获取信号量集的属性 struct semid_ds
第四个参数是一个 struct semid_ds 的指针,指向一块可用的空间,标识要把获取到的参数信息保存到结构体中去,例如:
struct semid_ds buf;semctl(semid,0,IPC_STAT,&buf); //获取信号量集的属性
IPC_SET:设置信号量集的属性 struct semid_ds
第四个参数是一个 struct semid_ds 的指针,指向的结构体是你要设置的目标结构体,例如:
struct semid_ds buf;semctl(semid,0,IPC_STAT,&buf);/*指定/修改 buf 中的某些成员的值...*/semctl(semid,0,IPC_SET,&buf);
备注:
对于同一个函数,第四个参数根据命令不同而需要不同的结构,系统在内部为此定义了一个 semun 联合体,来说明第四个参数的类型,所以一般我们传递一个 semun 的联合体指针给第四个参数就好了。
/* arg for semctl system calls. */
union semun {
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
unsigned short *array; /* array for GETALL & SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
void *__pad;
};
P/V 操作
1.semop 函数
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semop(int semid, struct sembuf *sops, size_t nsops);
作用:
对 semid 信号量集中的某个信号执行 P/V 操作
参数含义:
semid: 要操作的信号量集的 id;
sops: 指针,指向一个信号量 P/V 操作的描述数组,一个信号量的 P/V 操作是由struct sembuf 中的 sem_op 决定的,一个信号量的 P/V 操作需要一个 struct sembuf 描述,如果有多个 P/V 操作,需要多个 struct sembuf(数组),因为你可能会同时对多个信号量进行 P/V 操作 P(S1 & S2)
nsops: 第二个参数指向数组中的元素个数,表示需要进行多少个 P/V 操作
返回值:
成功返回 0,
失败返回-1,同时 errno 被设置
sembuf 结构体
struct sembuf {
unsigned short sem_num; /* 数组中的信号量索引 */
short sem_op; /* 信号量操作 */
short sem_flg; /* 操作标志 */
};
sem_num: 表示你要操作信号量集中的哪一个信号量
sem_op: 描述对信号量值(semval)的一种操作
semval = 原 semval+sem_op
sem_op < 0 =======> P 操作(减小信号量值)
sem_op > 0 =======> V 操作(增加信号量值)
sem_op = 0 表示调用者期望 semval 变为 0,如果为 0 则立即返回,如果不为 0, 则 semzcnt 加 1,调用者进程被阻塞
sem_flg: 默认为 0,表示阻塞等待,为 IPC_NOWAIT 为非阻塞,不等待,为 SEM_UNDO 则为撤销,这个标志非常有意义,是为了防止进程带锁退出的,如果设置了这个标志,内核会额外记录该进程对信号量的所有的 P/V 操作,在进程退出的时候,会还原所有的操作。
2. semtimedop 函数
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
const struct timespec *timeout);
作用:
对 semid 信号量集中的某个信号执行 P/V 操作,与 semop 函数不同的是,semop 要么是死等(阻塞),要么是不等待,而 semtimedop 可以限时等待
参数含义:
semid: 要操作的信号量集的 id;
sops: 指针,指向一个信号量 P/V 操作的描述数组,一个信号量的 P/V 操作是由struct sembuf 中的 sem_op 决定的,一个信号量的 P/V 操作需要一个 struct sembuf 描述,如果有多个 P/V 操作,需要多个 struct sembuf(数组),因为你可能会同时对多个信号量进行 P/V 操作 P(S1 & S2)
nsops: 第二个参数指向数组中的元素个数,表示需要进行多少个 P/V 操作
timeout: 等待时间
struct timespec
{
time_t tv_sec;//秒数
long tv_nsec;//纳秒
};
返回值:
成功返回 0,
失败返回-1,同时 errno 被设置
system V 信号量使用实例
实现进程间通信之间的资源竞争,通信方式采用共享内存
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include<string.h>
#include<stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/sem.h>
#define PATHNAME "/home/china"
#define PROJ_ID 20207 //共享内存
#define PROJ_ID2 20208
/*
Mysem_get:创建或者打开一个信号量
path:存在的路径名
id:大于 0 的整数
返回值:
成功返回创建好的信号量的 id
失败返回-1
*/
int Mysem_get(char *path,int id)
{
//产生一个 System V IPC key --->信号量
key_t key_sem = ftok(path,id);
if(-1 == key_sem)
{
perror("ftok error");
return -1;
}
//利用 key 创建或者打开一个 IPC 对象 --->信号量
int sem_id = semget(key_sem,1, IPC_CREAT | 0664);
if(sem_id == -1)
{
perror("semget error");
return -1;
}
return sem_id;
}
//初始化一个信号量
int Mysem_Init(int semid,ushort val)
{
//初始化信号量集的值为 1
int r = semctl(semid,0, SETALL,&val);
return r;
}
//P 操作
int Mysem_P(int semid)
{
struct sembuf op[1];
op[0].sem_num=0;//对信号量集中哪一个信号量操作
op[0].sem_op=-1; //P 操作
op[0].sem_flg = SEM_UNDO;
int r = semop(semid, op, 1);
return r;
}
//V 操作
int Mysem_V(int semid)
{
struct sembuf op[1];
op[0].sem_num=0;//对信号量集中哪一个信号量操作
op[0].sem_op=1; //V 操作
op[0].sem_flg = SEM_UNDO;
int r = semop(semid, op, 1);
return r;
}
//删除操作
int Mysem_Del(int semid)
{
return semctl(semid,0,IPC_RMID);
}
int main()
{
//产生一个 System V IPC key --->共享内存
key_t key = ftok(PATHNAME,PROJ_ID); //一个文件名和一个整数,来生成一个 key"钥匙"
if(-1 == key)
{
perror("ftok error");
exit(-1);
}
//利用 key 创建或者打开一个 IPC 对象 --->共享内存
int shm_id = shmget(key,4096,IPC_CREAT | 0664);
if(-1 == shm_id)
{
perror("shmget error");
exit(-1);
}
//对共享内存的操作(shmat/shmdt)
//把共享内存映射到本进程地址空间
int * p = (int *)shmat(shm_id,NULL, 0);
if(p == NULL)
{
perror("shmat error");
exit(-1);
}
//自由的读写共享内存区域
*p = 0; //初始化共享内存区域前面 4 字节
//---------------------------------------------
//产生一个 System V IPC key --->信号量
int sem_id = Mysem_get(PATHNAME,PROJ_ID2);
//初始化信号量集的值
Mysem_Init(sem_id,1); //初始化为 1 表示可用资源数为 1
pid_t pid = fork(); //创建子进程
if(pid > 0)
{
//父进程让共享内存区域前面 4 字节自加 100000 次
int i = 100000;
while(i--)
{
//lock(P 操作)
Mysem_P(sem_id);
(*p)++; //临界区
//unlock(V 操作)
Mysem_V(sem_id);
}
wait(NULL); //等待子进程结束
printf("Data:%d\n",*p); //200000
Mysem_Del(sem_id);
}else if(pid == 0)
{
//子进程让共享内存区域前面 4 字节自加 100000 次
int i = 100000;
while(i--)
{
//lock(P 操作)
Mysem_P(sem_id);
(*p)++; //临界区
//unlock(V 操作)
Mysem_V(sem_id);
}
}
//把共享内存分离
shmdt(p);
return 0;
}