`
isiqi
  • 浏览: 16488130 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

Linux进程间通信之信号量

阅读更多

第14章 信号量,共享内存与消息队列

在这一章,我们将会讨论Unix发行版AT&T系统V.2所引入的进程通信工具集合。因为这些程序出现在相关的发行版本中并且具有类似的编程接口,他们通常被称之为IPC程序,或是更为通常的System V IPC。正如我们已经了解到的,他们绝不是进程之间通信的唯一方法,但是System V IPC通常用来指这些特殊的程序。

在这一章,我们将会讨论下列内容:

用于管理资源访问的信号量
用于程序之间高效共享数据的共享内存
用于在程序之间简单传递数据的消息队列

信号量

当我们在多用户系统,多进程系统,或是两者混合的系统中使用线程操作编写程序时,我们经常会发现我们有段临界代码,在此处我们需要保证一个进程(或是一个线程的执行)需要排他的访问一个资源。

信号量有一个复杂的编程接口。幸运的是,我们可以很容易的为自己提供一个对于大多数的信号量编程问题足够高效的简化接口。

我们在第7章的第一个例子程序中--使用dbm访问数据库--如果多个程序尝试同时更新数据库,那么数据将会被破坏。两个不同的程序要求两个不同的用户为数据库输入数据则没有问题;问题的本质就在于更新数据库的代码部分。这些代码实际上执行数据更新并且需要排他的执行,就被称之为临界代码。通常他们只是一个大程序中的几行代码。

为了阻止多个程序同时访问一个共享资源所引起的问题,我们需要一种方法生成并且使用一个标记从而保证在临界区部分一次只有一个线程执行。我们在第12章简要的了解了一些线程相关的方法,我们可以使用互斥或信号量来控制一个多线程程序对于临界区的访问。在这一章,我们将会回到信号量这个话题,但是我们会了解如何更为通用的在不同的进程之间使用信号量。

编写通用目的的代码保证一个程序排他的访问一个特定的资源是十分困难的,尽管有一个名为Dekker的算法解决方法。不幸的是,这个算法依赖于"忙等待"或是"自旋锁",即一个进程的连续运行需要等待一个内存地址发生改变。在一个多任务环境中,例如Linux,这是对CPU资源的无谓浪费。如果硬件支持,这样的情况就要容易得多,通常以特定CPU指令的形式来支持排他访问。硬件支持的例子可以是访问指令与原子方式增加寄存器值,从而在读取/增加/写入的操作之间就不会有其他的指令运行。

我们已经了解到的一个要行的解决方法就是使用O_EXCL标记调用open函数来创建文件,这提供了原子方式的文件创建。这会使得一个进程成功的获得一个标记:新创建的文件。这个方法可以用于简单的问题,但是对于复杂的情况就要显得烦琐与低效了。

当Dijkstr引入信号量的概念以后,并行编程领域前进了一大步。正如我们在第12章所讨论的,信号量是一个特殊的变量,他是一个整数,并且只有两个操作可以使得其值增加:等待(wait)与信号(signal)。因为在Linux与UNIX编程中,"wait"与"signal"已经具有特殊的意义了,我们将使用原始概念:
用于等待(wait)的P(信号量变量)
用于信号(signal)的V(信号量变量)

这两字母来自等待(passeren:通过,如同临界区前的检测点)与信号(vrjgeven:指定或释放,如同释放临界区的控制权)的荷兰语。有时我们也会遇到与信号量相关的术语"up"与"down",来自于信号标记的使用。

信号量定义


最简单的信号量是一个只有0与1两个值的变量,二值信号量。这是最为通常的形式。具有多个正数值的信号量被称之为通用信号量。在本章的其余部分,我们将会讨论二值信号量。

P与V的定义出奇的简单。假定我们有一个信号量变量sv,两个操作定义如下:

P(sv) 如果sv大于0,减小sv。如果sv为0,挂起这个进程的执行。
V(sv) 如果有进程被挂起等待sv,使其恢复执行。如果没有进行被挂起等待sv,增加sv。

信号量的另一个理解方式就是当临界区可用时信号量变量sv为true,当临界区忙时信号量变量被P(sv)减小,从而变为false,当临界区再次可用时被V(sv)增加。注意,简单的具有一个我们可以减小或是增加的通常变量并不足够,因为我们不能用C,C++或是其他的编程语言来表述生成信号,进行原子测试来确定变量是否为true,如果是则将其变为false。这就是使得信号量操作特殊的地方。

一个理论例子

我们可以使用一个简单的理论例子来了解一下信号量是如何工作的。假设我们有两个进程proc1与proc2,这两个进程会在他们执行的某一时刻排他的访问一个数据库。我们定义一个单一的二值信号量,sv,其初始值为1并且可以为两个进程所访问。两个进程然后需要执行同样的处理来访问临界区代码;实际上,这两个进程可以是同一个程序的不同调用。

这两个进程共享sv信号量变量。一旦一个进程已经执行P(sv)操作,这个进程就可以获得信号量并且进入临界区。第二个进程就会被阻止进行临界区,因为当他尝试执行P(sv)时,他就会等待,直到第一个进程离开临界区并且执行V(sv)操作来释放信号量。

所需要的过程如下:

semaphore sv = 1;
loop forever {
P(sv);
critical code section;
V(sv);
noncritical code section;
}

这段代码出奇的简单,因为P操作与V操作是十分强大的。图14-1显示了P操作与V操作如何成为进行临界区代码的门槛。

Linux信号量工具

现在我们已经了解了什么是信号量以及他们在理论上是如何工作的,现在我们可以来了解一下这些特性在Linux中是如何实现的。信号量函数接口设计十分精细,并且提供了比通常所需要的更多的实用性能。所有的Linux信号量函数在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。乍看起来,这似乎使得事情变得更为复杂,但是在一个进程需要锁住多个资源的复杂情况下,在信号量数组上进行操作将是一个极大的优点。在这一章,我们将会关注于使用单一信号量,因为在大多数情况下,这正是我们需要使用的。

信号量函数定义如下:

#include <sys/sem.h>
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);

事实上,为了获得我们特定操作所需要的#define定义,我们需要在包含sys/sem.h文件之前通常需要包含sys/types.h与sys/ipc.h文件。而在某些情况下,这并不是必须的。

因为我们会依次了解每一个函数,记住,这些函数的设计是用于操作信号量值数组的,从而会使用其操作向比单个信号量所需要的操作更为复杂。

注意,key的作用类似于一个文件名,因为他表示程序也许会使用或是合作所用的资源。相类似的,由semget所返回的并且为其他的共享内存函数所用的标识符与由fopen函数所返回 的FILE *十分相似,因为他被进程用来访问共享文件。而且与文件类似,不同的进程会有不同的信号量标识符,尽管他们指向相同的信号量。key与标识符的用法对于在这里所讨论的所有IPC程序都是通用的,尽管每一个程序会使用独立的key与标识符。

semget

semget函数创建一个新的信号量或是获得一个已存在的信号量键值。

int semget(key_t key, int num_sems, int sem_flags);

第一个参数key是一个用来允许不相关的进程访问相同信号量的整数值。所有的信号量是为不同的程序通过提供一个key来间接访问的,对于每一个信号量系统生成一个信号量标识符。信号量键值只可以由semget获得,所有其他的信号量函数所用的信号量标识符都是由semget所返回的。

还有一个特殊的信号量key值,IPC_PRIVATE(通常为0),其作用是创建一个只有创建进程可以访问的信号量。这通常并没有有用的目的,而幸运的是,因为在某些Linux系统上,手册页将IPC_PRIVATE并没有阻止其他的进程访问信号量作为一个bug列出。

num_sems参数是所需要的信号量数目。这个值通常总是1。

sem_flags参数是一个标记集合,与open函数的标记十分类似。低九位是信号的权限,其作用与文件权限类似。另外,这些标记可以与IPC_CREAT进行或操作来创建新的信号量。设置IPC_CREAT标记并且指定一个已经存在的信号量键值并不是一个错误。如果不需要,IPC_CREAT标记只是被简单的忽略。我们可以使用IPC_CREAT与IPC_EXCL的组合来保证我们可以获得一个新的,唯一的信号量。如果这个信号量已经存在,则会返回一个错误。

如果成功,semget函数会返回一个正数;这是用于其他信号量函数的标识符。如果失败,则会返回-1。

semop

函数semop用来改变信号量的值:

int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

第一个参数,sem_id,是由semget函数所返回的信号量标识符。第二个参数,sem_ops,是一个指向结构数组的指针,其中的每一个结构至少包含下列成员:

struct sembuf {
short sem_num;
short sem_op;
short sem_flg;
}

第一个成员,sem_num,是信号量数目,通常为0,除非我们正在使用一个信号量数组。sem_op成员是信号量的变化量值。(我们可以以任何量改变信号量值,而不只是1)通常情况下中使用两个值,-1是我们的P操作,用来等待一个信号量变得可用,而+1是我们的V操作,用来通知一个信号量可用。

最后一个成员,sem_flg,通常设置为SEM_UNDO。这会使得操作系统跟踪当前进程对信号量所做的改变,而且如果进程终止而没有释放这个信号量,如果信号量为这个进程所占有,这个标记可以使得操作系统自动释放这个信号量。将sem_flg设置为SEM_UNDO是一个好习惯,除非我们需要不同的行为。如果我们确实变我们需要一个不同的值而不是SEM_UNDO,一致性是十分重要的,否则我们就会变得十分迷惑,当我们的进程退出时,内核是否会尝试清理我们的信号量。

semop的所用动作会同时作用,从而避免多个信号量的使用所引起的竞争条件。我们可以在手册页中了解关于semop处理更为详细的信息。

semctl

semctl函数允许信号量信息的直接控制:

int semctl(int sem_id, int sem_num, int command, ...);

第一个参数,sem_id,是由semget所获得的信号量标识符。sem_num参数是信号量数目。当我们使用信号量数组时会用到这个参数。通常,如果这是第一个且是唯一的一个信号量,这个值为0。command参数是要执行的动作,而如果提供了额外的参数,则是union semun,根据X/OPEN规范,这个参数至少包括下列参数:

union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
}

许多版本的Linux在头文件(通常为sem.h)中定义了semun联合,尽管X/Open确认说我们必须定义我们自己的联合。如果我们发现我们确实需要定义我们自己的联合,我们可以查看semctl手册页了解定义。如果有这样的情况,建议使用手册页中提供的定义,尽管这个定义与上面的有区别。

有多个不同的command值可以用于semctl。在这里我们描述两个会经常用到的值。要了解semctl功能的详细信息,我们应该查看手册页。

这两个通常的command值为:

SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
IPC_RMID:当信号量不再需要时用于删除一个信号量标识。

semctl函数依据command参数会返回不同的值。对于SETVAL与IPC_RMID,如果成功则会返回0,否则会返回-1。

使用信号量

正如我们在前面部分的描述中所看到的,信号量操作是相当复杂的。这是最不幸的,因为使用临界区进行多进程或是多线程编程是一个十分困难的问题,而其拥有其自己复杂的编程接口也增加了编程负担。

幸运的是,我们可以使用最简单的二值信号量来解决大多数需要信号量的问题。在我们的例子中,我们会使用所有的编程接口来创建一个非常简单的用于二值信号量的P
与V类型接口。然后,我们会使用这个简单的接口来演示信号量如何工作。

要试验信号量,我们将会使用一个简单的程序,sem1.c,这个程序我们可以多次调用。我们将会使用一个可选的参数来标识这个程序是负责创建信号量还是销毁信号量。

我们使用两个不同字符的输出来标识进入与离开临界区。使用参数调用的程序会在进入与离开其临界区时输出一个X,而另一个程序调用会在进入与离开其临界区时输出一个O。因为在任何指定的时间内只有一个进程能够进入其临界区,所以所有X与O字符都是成对出现的。

试验--信号量

1 在#include语句之后,我们定义函数原型与全局变量,然后我们进入main函数。在这里使用semget函数调用创建信号量,这会返回一个信号量ID。如果程序是第一次调用(例如,使用一个参数并且argc > 1来调用),程序就会调用set_semvalue来初始化信号量并且将op_char设置为X。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#include "semun.h"

static int set_semvalue(void);
static void del_semvalue(void);
static int semaphore_p(void);
static int semaphore_v(void);

static int sem_id;

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);

if(argc > 1)
{
if(!set_semvalue())
{
fprintf(stderr, "Failed to initialize semaphore\n");
exit(EXIT_FAILURE);
}
op_char = 'X';
sleep(2);
}
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);
3 在临界区之后,我们调用semaphore_v函数,在随机的等待之后再次进入for循环之后,将信号量设置为可用。在循环之后,调用del_semvalue来清理代码。
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);
}

4 函数set_semvalue在一个semctl调用中使用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;
}
5 del_semvalue函数几乎具有相同的格式,所不同的是semctl调用使用IPC_RMID命令来移除信号量ID:

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");
}

6 semaphore_p函数将信号量减1(等待):

static int semaphore_p(void)
{
struct sembuf sem_b;

sem_b.sem_num = 0;
sem_b.sem_op = -1;
sem_b.sem_flag = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_p failed\n");
return 0;
}
return 1;
}

7 semaphore_v函数将sembuf结构的sem_op部分设置为1,从而信号量变得可用。

static int semaphore_v(void)
{
struct sembuf sem_b;

sem_b.sem_num = 0;
sem_b.sem_op = 1;
sem_b.sem_flag = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
fprintf(stderr, "semaphore_v failed\n");
return 0;
}
return 1;
}

注意,这个简单的程序只有每个程序有一个二值信号量,尽管如果我们需要多个信号量,我们可以扩展这个程序来传递多个信号量变量。通常,一个简单的二值信号量就足够了。

我们可以通过多次调用这个程序来测试我们的程序。第一次,我们传递一个参数来通知程序他并不负责创建与删除信号量。另一次调用没有传递参数。

下面是两次调用的示例输出结果:

$ ./sem1 1 &
[1] 1082
$ ./sem1
OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX
1083 - finished
1082 - finished
$

正如我们所看到了,O与X是成对出现的,表明临界区部分被正确的处理了。如果这个程序在我们的系统上不能正常运行,也许我们需要在调用程序之前使用命令stty -tostop来保证生成tty输出的后台程序不会引起信号生成。

工作原理

这个程序由我们选择使用semget函数所获得的键生成一个信号量标识开始。IPC_CREAT标记会使得如果需要的时候创建一个信号量。

如果这个程序有参数,他负责使用我们的set_semvalue函数来初始化信号量,这是更为通用的semctl函数的一个简化接口。同时,他也使用所提供的参数来决定要输出哪一个字符。sleep只是简单的使得我们在这个程序执行多次之前有时间调用程序的另一个拷贝。在程序中我们使用srand与rand来引入一些伪随机计数。

这个程序循环十次,在其临界区与非临界区等待一段随机的时间。临界区代码是通过调用我们的semaphore_p与semaphore_v函数来进行保护的,这两个函数是更为通用的semop函数的简化接口。

在删除信号量之前,使用参数调用的程序拷贝会等待其他的调用结束。如果信号量没有删除,他就会继续存在于系统中,尽管已经没有程序再使用他。在实际的程序中,保证我们没有遗留信号是十分重要的。在我们下一次运行程序时,遗留的信号量会引起问题,而且信号量是限制资源,我们必须小心使用。

分享到:
评论

相关推荐

    Linux进程间通信-信号量通信进程互斥实例.pdf

    【Linux进程间通信】在操作系统中,进程间通信(Inter-Process Communication, IPC)是不同进程之间交换数据的重要机制。Linux提供了多种IPC方式,包括管道、消息队列、共享内存、信号量等。本实例主要关注的是...

    linux进程间通信与同步.pdf

    ### Linux进程间通信与同步详解 #### 一、概述 在多任务操作系统中,进程间通信与同步机制是解决进程间数据交换与资源共享的关键技术。这些机制确保了多个并发运行的任务能够有效地协作,并且避免了资源冲突。对于...

    linux进程间通信ppt

    【Linux进程间通信】是操作系统中进程协作的重要方式,它允许不同的进程共享数据和资源,协同完成任务。在Linux系统中,进程间通信(IPC,Inter-Process Communication)包括多种技术,如管道、信号、信号量、共享...

    UNIX Linux实验教程 4实验四Linux进程间通信.doc

    此外,UNIX/Linux 操作系统还提供了共享内存、信号量、套接口和全双工管道等其他进程间通信机制。共享内存是指多个进程可以访问同一个内存区域,信号量是指一种特殊的变量,用于多个进程之间的同步和通信。套接口是...

    Linux进程间通信.pdf

    ### Linux进程间通信详解 #### 引言 在现代操作系统如Linux中,进程间通信(IPC,Inter-Process Communication)是实现多进程协同工作的重要机制。通过IPC,不同进程能够共享信息、同步状态以及协作完成复杂的任务...

    linux 进程间通信

    本示例“Linux 进程间通信”着重演示了System V IPC机制,包括信号量(Semaphore, sem)、共享内存(Shared Memory, shm)和管道(FIFO)三种方式。这些技术广泛应用于构建复杂、高效的系统和服务,尤其是在需要不同...

    Linux进程间通信-信号量通信进程同步实例.pdf

    总结来说,这个实例展示了如何在Linux环境中利用信号量实现进程间的同步,控制并发执行的顺序,以防止数据不一致和错误。理解并熟练运用信号量机制对于编写多线程或多进程的高效、可靠软件至关重要。同时,掌握这样...

    windows 与linux 进程间通信区别

    为了帮助开发者了解如何将Windows平台上开发的使用IPC的应用程序移植到Linux,本文将详细探讨Windows与Linux在进程间通信方面的区别,并以此为基础提供迁移指导。 首先,同步技术是进程间通信中的重要组成部分。在...

    Linux进程间通信方式之socket使用实例

    ### Linux进程间通信方式之socket使用实例详解 #### 一、引言 在现代操作系统中,进程间的通信(IPC)是实现多进程协同工作的重要手段之一。Linux提供了多种进程间通信的方法,包括信号量、消息队列、共享内存以及...

    linux进程间通信——信号机制

    总结来说,Linux的信号机制是进程间通信的重要组成部分,它提供了简单但灵活的方式来处理系统事件和进程间的交互。`signal`和`sigaction`函数为程序员提供了处理信号的工具,使得我们可以根据需求定制信号的响应行为...

    Linux 进程间通信

    Linux提供了多种进程间通信的方式,包括但不限于管道、消息队列、信号量、共享内存等。本篇文章主要聚焦于管道和有名管道。 #### 二、管道概述及相关API应用 ##### 2.1 管道的相关概念 管道是Linux中最早的进程间...

    linux 操作系统进程间通信 ppt

    在 Linux 操作系统中,信号是一种非常重要的进程间通信机制,信号可以是同步的也可以是异步的。同步信号是指在程序中的某个具体操作相关的信号,例如程序错误等。异步信号是指接收该信号的进程控制之外的事件生成的...

    深刻理解Linux进程间通信

    ### 深刻理解Linux进程间通信 #### Linux进程间通信概述 进程间通信(Inter-Process Communication,简称IPC)是指在多进程环境下,不同进程之间进行数据交换或同步操作的技术。Linux作为一款广泛使用的开源操作...

    Linux多进程通信-信号量,共享内存示例

    在Linux操作系统中,多进程通信(IPC,Inter-Process Communication)是实现不同进程间数据交换的关键技术。本示例通过信号量和共享内存这两种IPC机制,展示了如何在多个进程中有效地协同工作。信号量用于同步对共享...

    深刻理解Linux进程间通信.pdf

    Linux进程间通信(IPC)是操作系统中非常重要的部分,它包括多种机制用于进程间的数据交换。了解这些机制对于进行系统编程和软件开发非常重要。本文档详细介绍了管道、信号、消息队列、信号量和共享内存等通信方法。...

    Linux进程间通信

    在Linux操作系统中,进程间通信(IPC,Inter-Process ...总结来说,Linux进程间通信是实现多进程协同工作、提高系统效率的关键。理解并灵活运用各种IPC方式,有助于我们设计出高效、可靠的多线程和多进程应用程序。

    使用共享内存及信号量实现进程间通信例子

    这个示例对于学习如何在C语言环境中使用Linux系统调用来实现进程间通信和同步非常有帮助。通过分析和运行`semtest`代码,开发者可以更深入地理解这些概念,并能够将其应用于自己的项目中。记住,良好的同步机制是...

    linux进程间通信讲义及代码.

    Linux进程间通信(IPC,Inter-Process Communication)是操作系统中一项关键的技术,它允许不同的进程之间交换数据,协同完成复杂的任务。本讲义主要面向初中级Linux学者,旨在深入理解并掌握这一领域的核心概念和...

    实验四进程间通信(信号量机制实验).ppt

    实验四进程间通信(信号量机制实验) 信号量机制是LINUX为进程中断处理而设置的,它只是一个组预定义的值,因而不能用于信息交换,只用于进程中断控制。信号量机制可以来源于硬件来源或软件来源。硬件来源包括按下...

Global site tag (gtag.js) - Google Analytics