Linux Program信号量共享内存和消息队列
Posted Jiamings
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux Program信号量共享内存和消息队列相关的知识,希望对你有一定的参考价值。
系列文章:
- 文件操作
- 数据管理
- 进程和信号
- POSIX 线程
- 进程间通信:管道
- 信号量共享内存和消息队列
- 套接字
文章目录
- 1. 信号量
本章将讨论一组进程间通信的机制,由于这些机制都出现在同一个版本中并且有着相似的编程接口,所以它们常被称为IPC(Inter-Process Communication,进程间通信)机制。
- 信号量:用于管理对资源的访问。
- 共享内存:用于在程序之间高效地共享数据。
- 消息队列:在程序之间传递数据的一种简单方法。
1. 信号量
当我们编写的程序使用了线程时,不管它是运行在多用户系统上、多进程系统上,还是运行在多用户多进程系统上,我们通常会发现,程序中存在着一部分临界代码,我们需要确保只有一个进程可以进入这个临界代码并拥有对资源独占式的访问权。
为了防止出现因多个程序同时访问一个共享资源而引发的问题,我们需要有一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域,我们可以在使用线程的程序中通过互斥量或信号量来控制对临界区的访问。
要想编写通用的代码,以确保程序对某个特定的资源具有独占式的访问权是非常困难的。虽然有一个名为 Dekker 算法的解决方法,但这个算法依赖于忙等待或自旋锁,也就是说,一个进程要持续不断地以等待某个内存位置被改变,在像 Linux 这样的多任务环境中,人们不愿意使用这种浪费 CPU 资源的处理方法,但如果硬件支持独占式访问(一般是通过特定的 CPU 指令的形式),那么情况就变得简单多了。一个硬件支持的例子就是,用一条指令以原子方式访问并增加寄存器的值,在这个读取/增加/写入操作执行的过程中不会有其他指令(甚至一个中断)发生。
一种可能的解决方法是,使用带有 O_EXCL 标志的 open 函数来创建锁文件,它提供了原子化的文件创建方法。它允许一个进程通过获取一个令牌来取得成功,这个方法比较适合处理简单问题,但对于更加复杂的例子,它就显得比较杂乱且缺乏效率。
荷兰科学家 Dijkstra 提出的信号量概念是在并发编程领域迈出的重要一步,信号量是一个特殊的变量,它只取整数值,并且程序对其访问都是原子操作,只允许对它进行等待(P,passeren,wait)和发送信号(V,vrijgeven,signal)两种操作。
1.1 信号量的定义
最简单的信号量是只能取值 0 和 1 的变量,即二进制信号量。PV 操作的定义非常简单,假设有一个信号量变量 sv,则这两个操作的定义如表:
P(sv): 如果 sv 的值大于零,就给它减去 1;如果它的值等于零,就挂起该进程。
V(sv): 如果有其它进程因为等待 sv 而被挂起,就让它恢复运行,如果没有进程因等待 sv 而被挂起,就给它加 1。
注意,只用一个普通变量进行类似的加减法是不行的,因为在 C、C++、C#或几乎任何一个传统的编程语言中,都没有一个原子操作可以满足检测变量是否为true,如果是再将变量设置为false的需要,这也是信号量操作如此特殊的原因。
1.2 Linux 的信号量机制
Linux 系统中的信号量接口经过了精心设计,它提供了比通常所需要更多的机制,所有的 Linux 信号量函数都是针对成组的通用信号量进行操作,而不是只针对一个二进制信号量。
int semctl(int sem_id, int sem_num, int command,....);
int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
这些函数都是用来对成组的信号量进行操作的,这使得对它们的操作要比单个信号量所需要的操作复杂的多。
semget 函数
该函数的作用是创建一个新信号量或取得一个已有信号量的键:
int semget(key_t key, int num_sems, int sem_flags);
第一个参数 key 是整数值,不相干的进程可以通过它访问同一个信号量,程序对所有信号量的访问都是间接的,它先提供一个键,再由系统生成一个相应的信号量标识符。只有 semget 函数才直接使用信号量键,所有其它的信号量函数都是使用由 semget 函数返回的信号量标志符。
有一个特殊的信号量键值 IPC_PRIVATE,它的作用是创建一个只有创建者进程才可以访问的信号量,但这个键值很少有实际的用途。在创建新的信号量时,你需要给键提供一个唯一的非零整数。
num_sems,指定信号量数目,它几乎总是取值为 1。
sem_flags 参数是一组标志,它与 open 函数的标志非常相似,它低端的 9 个比特是该信号量的权限,其作用类似于文件的访问权限。此外,它们还可以和值 IPC_CREAT 按位或操作,来创建一个新信号量。我们可以通过联合使用标志 IPC_CREAT 和 IPC_EXCL 来确保创建出的是一个新的、唯一的信号量。如果该信号量已存在,它将返回一个错误。
semop 函数
该函数用于改变信号量的值。
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
第一个参数 sem_id 是由 semget 返回的信号量标识符,第二个参数是指向一个结构数组的指针,每个数组元素至少包含以下几个成员:
struct sembuf
short sem_num; // 信号量编号,除非需要使用一组信号量,否则取值一般是 0
short sem_op、; // 信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是-1,一个是+1
short sem_flg; // 通常被设置为 sem_UNDO,使得操作系统跟踪当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量。除非你对信号量的行为有特殊的要求,否则应该养成设置 sem_flg 为 SEM_UNDO 的好习惯。
semop 调用的一切动作都是一次性完成的,这是为了避免出现因使用多个信号量而可能发生的竞争现象。
semctl 函数
该函数用来直接控制信号量信息。
int semctl(int sem_id, int sem_num, int command,....);
第一个参数 sem_id 是由 semget 返回的信号量标识符,sem_num 参数是信号量编号,当需要用到成组的信号量时,就要用到这个参数,它一般取值为 0,表示这是一个也是唯一一个信号量。command 参数是将要采取的动作。如果还有第四个参数,它将会是一个 union semun 结构,至少包含以下几个成员:
union semun
int val;
struct semid_ds *buf;
unsigned short *array;
虽然 command 参数可以设置许多不同的值,但只有下面介绍的两个值最常用:
- SETVAL: 用来把信号量初始化为一个已知的值。这个值通过 union semun 中的 val 成员设置,其作用是在信号量第一次使用之前对它进行设置。
- IPC_RMID: 用于删除一个已经无需继续使用的信号量标识符。
1.3 使用信号量
大部分需要使用信号量来解决的问题只需要一个最简单的二进制信号量即可,在下面的例子中,将用完整的编程接口为二进制信号量创建一个简单得多的 PV 类型的接口,然后用这个接口演示信号量是如何工作的。
我们用 seml.c 来试验信号量,该程序可以被多次调用,我们通过一个可选的参数来指定程序是负责创建信号量还是负责删除信号量。
我们用两个不同字符的输出来表示进入和离开临界区域,如果程序启动时带有一个参数,它将在进入和退出临界区时打印字符x,而程序的其他运行示例将在进入和退出临界区时打印字符 O,因为在任一给定时刻,只能有一个进程可以进入临界区域,所以字符X和O应该是成对出现的。
jiaming@jiaming-pc:~/Documents/test$ cc seml.c -o seml
jiaming@jiaming-pc:~/Documents/test$ cat run.sh
./seml 1 &
./seml
jiaming@jiaming-pc:~/Documents/test$ ./run.sh
OOOOXXOOXXOOXXOOXXOOXXOOOOOOXXOOX
8711 - finished
jiaming@jiaming-pc:~/Documents/test$ XXXXXXX
8710 - finished
union semun
int val;
struct semid_ds *buf;
unsigned short *arry;
;
static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);
static int sem_id;
// 调用semget来创建一个信号量,该函数将返回一个信号量标识符,如果程序是第一个被调用的,就调用 set_semvalue 初始化信号量,并将 op_char 设置为 X
int main(int argc, char *argv[])
int i;
int pause_time;
char op_char = O;
srand((unsigned int)getpid());
sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT); // 通过一个随机选取的键来取得一个信号量标识符,IPC_CREAT: 如果信号量不存在,就创建它
if(argc > 1)
if(!set_semvalue())
fprintf(stderr, "Failed to initialize semaphore\\n");
exit(EXIT_FAILURE);
op_char = X;
sleep(2);
// 接下来是一个循环,它进入和离开临界区域10次,在每次循环的开始,首先调用semaphore_p函数,它在程序将进入临界区域时设置信号量以等待进入:
for(i = 0; i < 10; i++)
if(!semaphore_p()) exit(EXIT_FAILURE);
printf("%c", op_char);fflush(stdout);
pause_time = rand() % 3;
sleep(pause_time);
printf("%c", op_char);fflush(stdout);
// 在临界区之后,调用semaphore_v来将信号量设置为可用,然后等待一段随机的时间,再进入下一次循环,在整个循环语句执行完毕后,调用del_semavalue函数来清理代码
if(!semaphore_v()) exit(EXIT_FAILURE);
pause_time = rand() % 2;
sleep(pause_time);
printf("\\n%d - finished\\n", getpid());
if(argc > 1)
sleep(10);
del_semvalue();
exit(EXIT_SUCCESS);
// 函数 set_semvalue 通过将 semctl 调用的 command 参数设置为 SETVAL 来初始化信号量,在使用信号量之前必须这样做。
static int set_semvalue(void)
union semun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1) return 0;
return 1;
static void del_semvalue(void)
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
fprintf(stderr, "Failed to delete semaphore\\n");
static int semaphore_p(void)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -Linux Program数据管理Linux进程间通信--进程,信号,管道,消息队列,信号量,共享内存