信号量机制
信号量(semaphore)是一种用于提供不同进程间或一个进程内部不同的线程间同步的机制.
进程/线程,任务: 并发的实体
同步 : 并发的实体间,相互等待相互制约,有序的,有条件的访问。
信号量的目的: 为了保护共享资源,让共享资源有序访问
保护实现
信号量的实质,其实是程序员之间的一种约定,用来保护共享资源的,
访问设备:
如进程A 和 B,都要访问一个互斥设备,那么可以用一个信号量来表示
能不能访问该设备,然后每个进程访问该设备时,先去访问信号量,
如果能访问设备则先把信号量设置成"NO", 然后再去访问互斥设备,访问完互斥设备,然后再把信号量设置成“YES”。
访问共享内存:
在访问共享内存前,先获取该信号量判断能否访问
lock --》 内容 -》 unlock
无法访问则等待,直到信号量变成能访问。
信号量使用
创建一个信号量,并设置他的最大同时访问数,来一个数-1,走一个+1,当小于0时,表示不能再有同时访问了,需要等待。
(1) 创建一个信号量: 要求调用者指定信号量的初始值。
初始值表示该信号量保护的共享资源,可以同时被多少个任务访问。
sem_val = 5 : 表示此时有5个进程或线程去同时访问它所保护的资源
sem_val = 1 : 表示此时只能1个进程或线程去同时访问它所保护的资源 “互斥信号量”
(2) 等待一个信号量
该操作会测试这个信号量的值,如果其值 <= 0, 那么它会等待(阻塞)
一旦其值 > 0, 这个时候,将它-1,并继续往下执行临界区的代码。
P操作,lock上锁
(3)释放信号量
该操作将信号量的值 +1, 其函数实现类似如下:
V操作,Unlock 解锁
信号量注意:
(1) 明确谁是共享资源,谁是需要保护的对象
(2) 确定“临界区”: 是指操作共享资源的代码区域
(3) 一个保护的对象,就需要一个信号量
死锁:
死锁-》百度百科
Linux内核信号量具体实现
1.system V 信号量
2.Posix 信号量
1.System V 信号量 计数信号量集(计数信号量数组)
---ftok-----> 获取System V IPC对象的key ----semget-----> 在内核中创建或打开一个System V的信号量 -------> P/V 操作
a.打开创建一个信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数
int semget(key_t key, int nsems, int semflg);
key: system V ipc对象的key(一般是ftok的返回值)
nsems: 你要创建的信号量集中的信号量的个数。
如果我们不是创建信号量集,而是打开已经存在的信号量集
此处的参数可以是0.一旦创建完了一个信号量集,其信号量
的个数就不能被改变啦。
semflg :
(1)创建 IPC_CREAT | 权限位
(2)打开 0
返回值:
成功返回system V 信号量集的id.
失败返回-1,errno被设置。
b.semctl: 控制操作(设置或获取信号量集中的某个或某些信号量的值)
int semctl(int semid, int semnum, int cmd, arg);
semid:要操作的信号量集的id(semget的返回值)
semnum: 表示您要操作信号量集中的哪一个信号量,就是信号量数组的下标,从0开始, 0 ,1,2,3.。。。,nsems-1
cmd: 命令号,常用的有:
GETVAL: 获取第semnum个信号量的值.
SETVAL: 设置第semnum个信号量的值.
GETALL: 获取这个信号量集所有的信号量的值.
SETALL: 设置这个信号量集所有的信号量的值.
IPC_RMID: 删除这个信号量集。
arg: 针对不同的命令号,第四个参数不一样
cmd == IPC_RMID, 第四个参数不要 如: semctl(sem_id, 0, IPC_RMID);
cmd == GETVAL, 第四个参数不要,函数的返回值(semctl)就表示你获取的那个信号量的值。
如: int val = semctl(sem_id, 1 , GETVAL);
用来获取sem_id指定的那个信号量集中下标为1的那个信号量的值。
cmd == SETVAL , 第四个参数应该是为int,表示要设置的值
如: int sem_val = 1;
int r = semctl(sem_id, 1, SETVAL, sem_val);
用来设置sem_id指定的信号量集的下标为1的信号量的值
cmd == GETALL , 第四个参数,应该为: unsigned short vals[]这个数组用来保存获取的每一个信号量的值的。
如: unsigned short vals[10];
int r = semctl(sem_id, 0, GETALL, vals);//vals[0] 保存的是下标为0的信号量的值
cmd == SETALL ,第四个参数,应该为: unsigned short vals[]
这个数组用来保存将要设置的每一个信号量的值的。
如:
unsigned short vals[10] = {1,1,1,1,1,1,1,1,1,1};
int r = semctl(sem_id, 0, SETALL, vals);
//下标为0的信号量的值 => vals[0]
返回值:
根据不同的命令号,返回值不一样。
cmd == GETVAL ,返回值就是获取到的那个信号量的值
0 --> 成功
1 --> 失败
c. semop : System V 信号量的P/V操作
在System V 的信号量的P/V操作,用一个结构体来描述的
struct sembuf{
unsigned short sem_num; /* semaphore number */
要进行p/v操作的信号量在信号量集中的编号(下标)
short sem_op; /* semaphore operation */
> 0:=》V操作,up/unlock
= 0:try - try ,看是否会阻塞
< 0: => P操作,down/lock
short sem_flg;
//0:默认,如果P操作获取不了,则会阻塞
//IPC_NOWAIT: 非阻塞
如果是P操作,能获取则获取,不能获取
则走人(返回-1)
//SEM_UNDO:(撤销)
为了防止进程带锁退出
内核额外记录该进程对该信号量的所有的P/V操作,
然后在退出该进程时,会还原对该信号的操作。
};
一个struct sembuf表示对一个信号的P/V操作:
如果对两个信号量的P/V操作,就需要两个struct sembuf
如果对多个信号量的P/V操作,就需要多个struct sembuf
int semop(int semid, struct sembuf *sops, size_t nsops);
semid: 要操作哪个信号量集
sops: 指向一个结构体数组
struct sembuf 的数组
struct sembuf[0] -> 表示第0个信号量的P/V操作
struct sembuf[1] -> 表示第1个信号量的P/V操作
.。。。。
nsops: 第二个参数数组中元素的个数
返回值:
成功返回0
失败返回-1,errno被设置。
NOTE:
semop 可能会阻塞当前的进程/线程,如果是P操作,
获取不了的时候,且IPC_NOWAIT没有设置时,就会
“死等”
semtimedop: 限时等待
第四个参数timeout描述 “限时”: 如果在这个段时间内,没等到也返回
struct timespec {
long tv_sec; /* seconds */
//秒数
long tv_nsec; /* nanoseconds */
//纳秒
1s = 1000ms
1ms = 1000 us
1us = 1000 ns
};
ru: 等待5s
struct timespec tv;
tv.tv_sec = 5;
tv.tv_nsec = 0;
int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct timespec *timeout);
前面的三个参数以及返回值和上面的semop是一样的。
2.POSIX semaphore 单个信号量
有名信号量: 可以用于进程间或线程间的同步/互斥
在文件系统中有一个入口(有一个文件名,inode)
信号量的对象(值)去是在内核。
=> 即可用于进程, 也可用于线程的同步
无名信号量:
没有名字,无名信号量存在于内存。
无名信号量,“基于内存的信号量”
a、如果这段内存在一个 "内核共享内存中",进程可以访问,
线程也可以访问。 => 即可用于进程,
也可用于线程的同步
b、如果这段内存在一个 进程用户空间内 ,此时,只能用于
进程内部的线程的同步或互斥。
A.打开/创建一个posix信号量
POSIX信号量用sem_t来表示,不管你是有名还是无名的信号量都是用sem_t来描述的。
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
有名信号量
函数:
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
name:要创建或打开的POSIX有名信号量在文件系统中的路径名。(以'/'开头的路径,路径名中只能由一个'/')
如: "/test.sem"
oflag:
(1) 打开 0
(2) 创建 O_CREAT
第三个和第四个参数,当你是创建的时候才需要
mode : 权限,有两种方式去指定
(1) S_IRUSR ....
(2) 0664
value: 指定创建有名信号量的初始值。
返回值:
成功返回这个有名信号量的首地址
失败返回SEM_FAILED,errno被设置。
Link with -pthread.
在编译的时候 -l pthread,指定pthread库的名字。
=========================
无名信号量:
(1) 定义或分配一个无名信号量 sem_t
sem_t t1;
sem_t * psem = malloc(sizeof(sem_t));
(2)初始化无名信号量 sem_init
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:指向要初始化的无名信号量
pshared: 该无名信号量的共享方式
0 :进程内部空间的线程共享 sem指向的地址是进程内部的地址了。
1 :不同进程间的共享 sem指向的地址是内核共享内存区域。
value :指定无名信号量的初始值的
返回值:
成功 0
失败返回-1,errno被设置。
B.POSIX 信号量 P / V操作
P操作:
#include <semaphore.h>
1.“死等”
int sem_wait(sem_t *sem);
返回值:
返回0,获取了该信号量
返回-1,出错,同时errno被设置。
2.“try 一 try ”: 非阻塞的版本
int sem_trywait(sem_t *sem);
能获取则获取,不能获取返回-1
3.“限时等待的版本”
abs_timeout: 等待超时的绝对时间
“绝对时间”: = 当前时间 + 相对时间
“相对时间”:
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
Link with -lpthread.
struct timespec {
long tv_sec; /* seconds */
//秒数
long tv_nsec; /* nanoseconds */
//纳秒
1s = 1000ms
1ms = 1000 us
1us = 1000 ns
};
“绝对时间”: = 当前时间 + 相对时间
clock_gettime //获取当前时间
#include <time.h>
int clock_gettime(clockid_t clk_id, struct timespec *tp);
如: 等待5s 30ms
struct timespec ts;
clock_gettime(CLOCK_REALTIME , &ts); //获取当前时间
//等待5s 30ms
ts.tv_sec += 5;
ts.tv_nsec += 30000000;
if(ts.tv_nsec >= 100000000)
{
ts.tv_sec ++;
ts.tv_nsec -= 1000000000;
}
int r = sem_timedwait(psem, &ts);
V操作:
#include <semaphore.h>
sem_post:用来释放sem指定的POSIX信号量
int sem_post(sem_t *sem);
C.POSIX其他操作
a、sem_getvalue:用来获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);
sem: sem_t指针,表示要获取哪个信号量的值
sval: int * ,用来保存获取到的信号量的值。
返回值:
成功返回0
失败返回-1,errno被设置。
如:
int val;
int ret = sem_getvalue(psem, &val);
b、POSIX有名信号量的 关闭或删除 操作
sem_close: 用来关闭一个POSIX有名信号量
sem_unlink:用来删除一个POSIX有名信号量
int sem_close(sem_t *sem)
int sem_unlink(const char *name);
c、POSIX无名信号量的 销毁操作
sem_destroy:用来销毁一个POSIX无名信号量的
int sem_destroy(sem_t *sem);