1. 简述
Linux的同步于互斥大致可以分为三个方式,分别是互斥锁,条件变量和信号量。他们针对不同的应用场景而被设计出来,其中互斥锁主要用于对共享资源的保护,条件变量用于控制操作的顺序,而信号量则用来控制对共享资源的访问数量。
Linux信号量是一种用于进程间同步和互斥的低级同步机制。信号量可以在多个进程或线程之间协调对共享资源的访问。在Linux中,主要有两种信号量:POSIX信号量和System V信号量。它们各自有不同的特性和用途。
2. POSIX与System V信号量
目前我们在Linux系统中常用的信号量主要有两种,分别是POSIX信号量和System V信号量。他们各自有对应的特点和应用场景。
POSIX:主要用于线程间的同步(无名信号量),也可以用在进程间(有名信号量)。
System V:常作为一种IPC机制,应用在进程间实现同步。
综上,我们常说线程间实现同步和互斥,进程间通信都有信号量存在。
线程间同步和互斥手段:互斥锁、信号量、条件变量;
进程间通信(IPC):套接字、共享内存、消息队列、信号、信号量、管道;
3. POSIX信号量
我们在前面已经讲过,POSIX信号量既可以用在线程间也可以用在进程间同步,但实际上我们使用的形式是不一样的。
POSIX信号量有两种:
无名信号量:基于内存的信号量,存储在内存中,一般用于线程间的同步;
有名信号量:基于系统文件的信号量,依赖于系统中的文件,一般用于进程间的同步,有名信号量一般用在关联进程间的通信,如通过fork实现的多进程;
(1)无名信号量
无名信号量寄生于内存中,用于线程间同步。具体的API如下所示。
首先要引入相关头文件:
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>
初始化信号量
需要注意的是,尽管sem_init的第二个参数为非0时,可以用于进程间同步,但我们一般不会这么用,因为不同的平台支持不一样。对于进程间同步还是建议使用有名信号量。
/*
*初始化sem_t 变量的值。
* @param[out] sem sem_t变量的地址
* @param[in] pshared 信号量的作用域
* 为0时,信号量在同一进程的多线程间是共享的,用于线程间
* 非0时,信号量用于多进程间,且分配的sem_t内存必须处于多进程的共享内存区
* @param[in] value 信号量的初始值
* @return 成功 信号量标识, 失败 -1
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
销毁信号量
/*
*销毁sem指向的信号量,并释放所占用的资源
* @param[in] sem 指向信号量的指针
* @return 成功 信号量标识, 失败 -1
*/
int sem_destroy(sem_t * sem);
(2)有名信号量
创建有名信号量
/*
* 创建或打开一个已存在的有名信号量。
* 有名信号量既可用于线程间的同步, 也可用于进程间的同步
* @param[in] name
* @param[in] oflag 可以是0、O_CREAT或O_ CREAT|O_EXCL,如果指定了O_CREAT,需要第三、四个参数
* @param[in] mode 读写权限 eg:0666
* @param[in] value 指定信号量的个数
* @return 成功 信号量标识, 失败 -1
*/
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
关闭有名信号量
需要注意,关闭有名信号量以后,并没有把它从系统中清除。
/*
* 关闭信号量
* @param[in] psem sem_open的返回值
* @return 成功 0, 失败 -1
*/
int sem_close(sem_t *psem);
销毁信号量
/* @function 销毁信号量
* @param[in] name 信号量关联的文件
* @return 成功 0, 失败 -1
*/
int sem_unlink(const char *name);
(3)无名信号量和有名信号量的操作
除了初始化和创建方式不一样,有名和无名信号量的操作是相同的。
P操作
P操作就是对信号量执行-1操作。如果原值>0,那么-1操作以后就可以立即返回继续执行下面的操作。如果原值为0,那么当前线程将会进入阻塞休眠状态,等待其他线程+1。
#include <semaphore.h>
/*
* 阻塞式等待信号量的值大于0时,并减1,线程执行后续操作,如果为0,使线程进入睡眠
* @param[in] sem 指向信号量的指针或信号量的地址值
* @return 成功 0, 失败 -1
* @note 如果被某个信号中断, sem wait就可能过早地返回,所返回的错误为EINTR
*/
int sem_wait(sem_t *sem);
/*
* 非阻塞式等待信号量,没有等到不会进入睡眠,会返回一个EAGAIN错误
* @param[in] sem 指向信号量的指针或信号量的地址值
* @return 成功 0, 失败 -1
*/
int sem_trywait(sem_t *sem);
/*
* 定时阻塞等待信号量,时间到达没有等到不会进入睡眠,会返回一个ETIMEDOUT错误
* @param[in] sem 指向信号量的指针或信号量的地址值
* @param[in] abs_timeout 设置时间
* @return 成功 0, 失败 -1
*/
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
V操作
V操作与P操作相反,执行+1操作,用于唤醒其他正在阻塞休眠的线程。
/*
* +1,唤醒正在等待该信号量值为正数的任一线程。
* @param[in] sem 指向信号量的指针或信号量的地址值
* @return 成功 0, 失败 -1
*/
sem_post( sem_t *psem ); // V 操作(加一)
获取当前信号量的值
/*
* 获取信号量当前值
* @param[in] sem 指向信号量的指针或信号量的地址值
* @param[out] sval 由sval 指向的整数中返回所指定信号量的当前值
* @return 成功 0, 失败 -1
* @note 如果该信号量当前已上锁,那么返回值或为0,或为某个负数,其绝对值就是等待该信号量解锁的线程数
*/
int sem_getvalue(sem_t * sem, int * sval);
4. POSIX信号量例程
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/mman.h>
/** 信号量名称 */
#define SEM_NAME "/task_sem"
int main(int argc, char* argv[])
{
sem_t *semaphore;
pid_t pid;
/** 打开或创建一个名为SEM_NAME的信号量,初始值为1. */
semaphore = sem_open(SEM_NAME, O_CREAT, 0644, 1);
if (semaphore == SEM_FAILED) {
perror("sem_open");
exit(1);
}
pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) { ///< 子进程
printf("Child process is running...\n");
sem_wait(semaphore); ///< 等待信号量
printf("Child process has the semaphore and is performing work...\n");
sleep(3); ///< 模拟工作
sem_post(semaphore); ///< 释放信号量
printf("Child process has released the semaphore and finished work.\n");
} else { ///< 父进程
printf("Parent process is running...\n");
sleep(1); ///< 等待子进程先运行
sem_wait(semaphore); ///< 等待信号量
printf("Parent process has the semaphore and is performing work...\n");
sleep(3); ///< 模拟工作
sem_post(semaphore); ///< 释放信号量
printf("Parent process has released the semaphore and finished work.\n");
wait(NULL); ///< 等待子进程结束
}
sem_close(semaphore); ///< 关闭信号量
sem_unlink(SEM_NAME); ///< 删除信号量
return 0;
}
5. System V信号量
System V信号量作为一种IPC机制,常用在进程间的同步(理所当然的,也可以用在线程间的同步)。
(1)构建key_t
需要注意,使用ftok生成key_t数据时,需要保证对应的路径名存在。如果要在不同进程进进行同步,那么该路径名不能够被删除和重建,否则会导致生成的key_t不相同,而造成进程间无法实现同步。
key_t ftok(const char *pathname, int proj_id);
(2)获得信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
/*
* 创建一个信号量集或访问一个已存在的信号量集。
* @param[in] key 长整型唯一非零值,通常情况下,该id值通过ftok函数得到,或者自行设定一个长整型值。
* @param[in] nsems 指定集合中的信号量数,通常为1。如果我们不创建一个新的信号量集,而只是访问一个已存在的集合,那就可以把该参数指定为0。
* @param[in] flags 一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。
* 设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。
* 而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。也可以是SEM_R和ISEM_A常值的组合。
* @return 成功:信号量标识符,失败: -1
*/
int semget(key_t key, int nsems, int flags)
(3)控制信号量
/*
* 控制信号量信息
* @param[in] semid 由semget返回的信号量标识符
* @param[in] semnum 为要进行操作的集合中信号量的编号,当要操作到成组的信号量时,
* 从 0 开始。一般取值为 0,表示这是第一个也是唯一的一个信号量
* @param[in] cmd
* @arg IPC_RMID(立即删除信号集,唤醒所有被阻塞的进程)
* @arg GETVAL(根据semun 指定的编号返回相应信号的值, 此时该函数返回值就是你要获得的信号量的值,不再是 0 或-1)
* @arg SETVAL(根据 semun 指定的编号设定相应信号的值)
* @arg GETALL(获取所有信号量的值,此时第二个参数为 0,并且会将所有信号的值存入 semun.array 所指向的数组的各个元素中,此时需要用到第四个参数 union semun arg)
* @arg SETALL(将 semun.array 指向的数组的所有元素的值设定到信号量集中,此时第二个参数为 0,此时需要用到第四个参数 union semun arg)
* @return 成功 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 *arry; //GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};
(4)PV操作
struct sembuf{
short sem_num; ///< 要操作的信号量在信号量集中的编号,第一个信号量的编号是 0。
short sem_op; ///< sem_op 成员的值是信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是-1,也就是 p(减一)操作,它等待信号量变为可用;一个是+1,也就是 v(加一)操作,它发送信号通知信号量现在可用。
short sem_flg; ///< SEM_UNDO时当信号量小于0时会阻塞,设置为IPC_NOWAIT则不会阻塞。
};
/*
* 对semid标识的信号量集中的一个信号量或多个信号量进行操作
* @param[in] semid 由 semget 返回的信号量标识符
* @param[in] sops 指向一个结构体数组的指针。可以指向单个或者多个结构体变量(即结构体数组)。
* @param[in] nsops 为sops 指向的 sembuf 结构数组的长度
* @return 成功 0, 失败 -1
*/
int semop(int semid, struct sembuf *sops, size_t nsops)
6. System V信号量例程
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include <unistd.h>
#define SEM_KEY 9999 ///< 信号量的键值,用于标识信号量集[也可以用ftok获得].
int main() {
int semid; ///< 信号量集ID
struct sembuf op; ///< 信号量操作结构
pid_t pid;
/** 创建信号量集,其中只有一个信号量. */
semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget failed");
exit(1);
}
/** 初始化信号量的值为1. */
if (semctl(semid, 0, SETVAL, 1) == -1) {
perror("semctl SETVAL failed");
exit(1);
}
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
} else if (pid == 0) { ///< 子进程
printf("Child process is running...\n");
/** P操作:等待信号量. */
op.sem_num = 0;
op.sem_op = -1; ///< 减1操作
op.sem_flg = 0;
if (semop(semid, &op, 1) == -1) {
perror("semop -1 failed in child");
exit(1);
}
printf("Child process has the semaphore and is performing work...\n");
sleep(3); ///< 模拟工作
/** V操作:释放信号量. */
op.sem_op = 1; ///< 加1操作
if (semop(semid, &op, 1) == -1) {
perror("semop +1 failed in child");
exit(1);
}
printf("Child process has released the semaphore and finished work.\n");
} else { ///< 父进程
sleep(1); ///< 让子进程先运行
printf("Parent process is running...\n");
/** P操作:等待信号量. */
op.sem_num = 0;
op.sem_op = -1; ///< 减1操作
op.sem_flg = 0;
if (semop(semid, &op, 1) == -1) {
perror("semop -1 failed in parent");
exit(1);
}
printf("Parent process has the semaphore and is performing work...\n");
sleep(3); ///< 模拟工作
/** V操作:释放信号量. */
op.sem_op = 1; ///< 加1操作
if (semop(semid, &op, 1) == -1) {
perror("semop +1 failed in parent");
exit(1);
}
printf("Parent process has released the semaphore and finished work.\n");
wait(NULL); ///< 等待子进程结束
/** 删除信号量集. */
semctl(semid, 0, IPC_RMID);
}
return 0;
}