linux 进程间通信 信号,linux 进程信号
在本章中,我们将讨论由AT T system V.2(一个Unix发行版)引入的进程通信工具的集合。因为这些程序出现在相关的发行版中,并且具有相似的编程接口,所以它们通常被称为IPC程序,或者更常见的是System V IPC。正如我们已经了解到的,它们绝不是进程间通信的唯一方式,但System V IPC通常用于指代这些特殊的程序。
在本章中,我们将讨论以下内容:
用于管理资源访问的信号量
共享内存,实现程序间高效的数据共享
用于在程序间简单传输数据的消息队列。
旗语
当我们在多用户系统、多进程系统或混合系统中编写程序时,我们经常会发现我们有一个关键代码,在这里我们需要确保一个进程(或一个线程的执行)需要独占访问一个资源。
Semaphore有一个复杂的编程接口。幸运的是,我们可以很容易地为自己提供一个简化的接口,对于大多数信号量编程问题来说,这个接口足够有效。
在我们第7章的第一个示例程序中——使用dbm访问数据库——如果多个程序试图同时更新数据库,数据将被破坏。如果两个不同的程序需要两个不同的用户为数据库输入数据,这没有问题;问题的本质在于更新数据库的代码部分。这些代码实际上执行的是数据更新,需要独占执行,所以被称为关键代码。通常它们只是一个大程序中的几行代码。
为了防止多个程序同时访问一个共享资源而导致的问题,我们需要一种方法来生成并使用一个标志,以确保一次只有一个线程在临界区执行。在第12章,我们简要了解一些线程相关的方法。我们可以使用互斥或信号量来控制多线程程序对临界区的访问。在这一章中,我们将回到信号量的主题,但是我们将学习如何更普遍地在不同的进程之间使用信号量。
编写通用代码来保证一个程序对特定资源的独占访问是非常困难的,虽然有一个叫Dekker的算法。不幸的是,这种算法依赖于“忙等待”或“自旋锁”,即连续运行的进程需要等待内存地址发生变化。在多任务环境中,比如Linux,这是对CPU资源的浪费。如果硬件支持的话,这种情况就简单多了,通常以特定CPU指令的形式支持独占访问。硬件支持的例子可以是以原子方式访问指令和增加寄存器值,以便在读/加/写操作之间没有其他指令运行。
我们已经了解到这个问题的解决方案是使用O_EXCL标签来调用open函数来创建文件,它提供了原子文件创建。这将使一个进程成功地获得一个标记:新创建的文件。这种方法可以用于简单的问题,但对于复杂的情况会很繁琐,效率很低。
当Dijkstr引入信号量的概念时,并行编程领域向前迈进了一大步。正如我们在第12章中讨论的,信号量是一个特殊的变量,它是一个整数,只有两个操作可以增加它的值:等待和信号。因为‘等待’和‘信号’在Linux和UNIX编程中有特殊的含义,所以我们将使用最初的概念:
p(等待的信号量变量)
信号的v(信号量变量)
这两个字母来自荷兰语waiting (passeren:通过,像临界区前面的检查点)和signaling (vrjgeven:指定或释放,像释放临界区的控制权)。有时我们还会遇到与信号量相关的术语‘up’和‘down’,它们来自于信号标记的使用。
定义信号量
最简单的信号量是只有0和1值的变量,即二进制信号量。这是最常见的形式。有多个正值的信号量称为一般信号量。在本章的剩余部分,我们将讨论二进制信号量。
p和v的定义出奇的简单。假设我们有一个信号量变量sv,两个操作定义如下:
P(sv)如果sv大于0,则减小sv。如果sv为0,则暂停该进程的执行。
V(sv)如果有任何进程被挂起并等待sv,让它恢复执行。如果没有挂起的等待sv,则增加sv。
理解信号量的另一种方式是,信号量变量sv在临界区可用时为真,在临界区忙时减P(sv),变为假,在临界区再次可用时增V(sv)。注意,仅仅有一个我们可以减少或增加的公共变量是不够的,因为我们无法用C、C或其他编程语言来表达生成的信号,并进行原子测试来确定变量是否为真,如果是,则将其变为假。这就是信号量操作的特殊之处。
理论上的例子
我们可以用一个简单的理论例子来理解信号量是如何工作的。假设我们有两个进程,proc1和proc2,它们将在执行期间的某个时刻独占访问数据库。我们定义了一个二进制信号量sv,它的初始值为1,可以被两个进程访问。然后,这两个进程需要执行相同的处理来访问临界区代码;实际上,这两个进程可以是同一个程序的不同调用。
两个进程共享sv信号量变量。一旦一个进程执行了P(sv)操作,该进程就可以获得信号量并进入临界区。第二个进程会被阻止去临界区,因为当他试图执行P(sv)时,他会等到第一个进程离开临界区执行V(sv)操作来释放信号量。
所需的过程如下:
信号量SV=1;
永远循环{
p(SV);
关键代码段;
v(SV);
非关键代码段;
}
这段代码出奇的简单,因为P操作和V操作都很厉害。图14-1显示了P操作和V操作如何成为临界段代码的阈值。
Linux信号量工具
现在我们知道了信号量是什么,以及它们在理论上是如何工作的,我们可以学习这些特性是如何在Linux中实现的。信号量接口设计非常精细,它提供了比通常更实用的性能。的所有Linux信号量函数都在通用信号量数组上操作,而不是在单个二进制信号量上操作。乍一看,这似乎使事情变得更加复杂,但当一个进程需要锁定多个资源时,在信号量数组上操作将是一个很大的优势。在这一章中,我们将着重于使用单个信号量,因为在大多数情况下,这正是我们需要使用的。
信号量函数定义如下:
#包含系统/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 *非常相似,因为它是被进程用来访问共享文件的。与文件类似,不同的进程会有不同的信号量标识符,即使它们指向同一个信号量。这里讨论的所有IPC程序都使用键和标识符,尽管每个程序都使用自己的键和标识符。
信号量函数
Semget函数创建一个新的信号量或获取一个现有的信号量键值。
int semget(key_t key,int num_sems,int SEM _ flags);
第一个参数key是一个整数值,用于允许不相关的进程访问同一个信号量。的所有信号量都是通过为不同的程序提供一个键来间接访问的,并且为每个信号量系统生成一个信号量标识符。信号量键值只能由semget获取,其他所有信号量函数使用的信号量标识符都由semget返回。
还有一个特殊的信号量键值IPC_PRIVATE(通常为0),用于创建一个只能由创建进程访问的信号量。这通常没有什么有用的目的,但幸运的是,在一些Linux系统上,手册页将IPC_PRIVATE列为一个不阻止其他进程访问信号量的bug。
num_sems参数是所需的信号量数量。该值始终为1。
Sem_flags参数是一组标签,与open函数的标签非常相似。低9位是信号的权限,类似于文件权限。此外,这些标志可以与IPC_CREAT进行或操作,以创建一个新的信号量。设置IPC_CREAT标志并指定一个现有的信号量键值不是错误的。如果不需要,IPC_CREAT标志将被忽略。我们可以使用IPC_CREAT和IPC_EXCL的组合来确保我们可以获得一个新的和唯一的信号量。如果这个信号量已经存在,它将返回一个错误。
如果成功,semget函数将返回一个正数;这是用于其他信号量功能的标识符。如果失败,它将返回-1。
信号量操作
Semop函数用于改变信号量的值:
int semop(int sem_id,struct sembuf *sem_ops,size _ t num _ SEM _ ops);
第一个参数sem_id是semget函数返回的信号量标识符。第二个参数sem_ops是指向结构数组的指针,每个结构至少包含以下成员:
结构sembuf {
短sem _ num
短sem _ op
短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函数允许直接控制信号量信息:
int semctl(int sem_id,int sem_num,int command,);
第一个参数sem_id是semget获得的信号量标识符。Sem_num参数是信号量的数量。当我们使用信号量数组时,会用到这个参数。通常,如果这是第一个也是唯一的信号量,该值为0。命令参数是要执行的操作,如果提供了附加参数,它就是union semun。根据X/OPEN规范,该参数至少包括以下参数:
工会联合会
int val
struct semid _ ds * buf
无符号短*数组;
}
很多版本的Linux在头文件(通常是sem.h)中定义了semun联合,虽然X/Open确认我们必须定义自己的联合。如果我们发现我们真的需要定义我们自己的联合,我们可以查看semctl手册页中的定义。如果是这样,建议使用手册页中提供的定义,尽管这个定义与上面的定义不同。
有几种不同的命令值可用于semctl。这里我们描述两个经常使用的值。要了解更多关于semctl的功能,我们应该查看手册页。
两个常见的命令值是:
SETVAL:用于将信号量初始化为一个已知值。所需的值作为union semun的val成员传递。信号量需要在第一次使用之前设置。
IPC_RMID:用于在不再需要信号量时删除信号量ID。
Semctl函数将根据命令参数返回不同的值。对于SETVAL和IPC_RMID,成功则返回0,否则返回-1。
使用信号量
从前面的描述可以看出,信号量操作是相当复杂的。这是最不幸的,因为使用临界区进行多进程或多线程编程是一个非常困难的问题,其本身复杂的编程接口也增加了编程负担。
幸运的是,我们可以使用最简单的二进制信号量来解决大多数需要信号量的问题。在我们的例子中,我们将使用所有编程接口为二进制信号量创建一个非常简单的P。
V型接口。然后,我们将使用这个简单的接口来演示信号量是如何工作的。
为了测试信号量,我们将使用一个简单的程序sem1.c,我们可以多次调用它。我们将使用一个可选参数来标识这个程序是负责创建信号量还是负责销毁信号量。
我们使用两个不同字符的输出来标识进入和退出关键区域。用参数调用的程序在进入和离开其临界区时会输出一个X,而另一个程序调用在进入和离开其临界区时会输出一个O。因为在任何给定的时间只有一个进程可以进入它的临界区,所以所有的X和O字符都成对出现。
测试信号
在1 #include语句之后,我们定义函数原型和全局变量,然后我们进入主函数。这里,信号量是通过使用semget函数调用创建的,它将返回一个信号量ID。如果是第一次调用程序(比如使用一个参数,用argc 1调用),程序会调用set_semvalue来初始化信号量,并将op_char设置为x。
#包含stdio.h
#包含stdlib.h
#包括unistd.h
#包含sys/types.h
#包含sys/ipc.h
#包含系统/sem.h
#包含“semun.h”
静态int set _ SEM value(void);
静态void del _ SEM value(void);
静态int semaphore _ p(void);
静态int semaphore _ v(void);
静态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)
{
如果(!set_semvalue())
{
fprintf(stderr,未能初始化信号量/n );
退出(EXIT _ FAILURE);
}
op _ char= X
睡眠(2);
}
然后我们用一个循环码进出临界区10次。这时会调用semaphore_p函数,这个函数会设置信号量,等待程序进入临界段。
for(I=0;我我)
{
如果(!semaphore_p())退出(EXIT _ FAILURE);
printf(%c ,op _ char);fflush(stdout);
pause _ time=rand()% 3;
睡眠(pause _ time);
printf(%c ,op _ char);fflush(stdout);
3在临界段之后,我们调用semaphore_v函数,在随机等待一段时间后再次进入for循环后,将信号量设置为可用。循环结束后,调用del_semvalue清理代码。
如果(!semaphore_v())退出(EXIT _ FAILURE);
pause _ time=rand()% 2;
睡眠(pause _ time);
}
printf(/n%d - finished/n ,getpid());
if(argc 1)
{
睡眠(10);
del _ SEM value();
}
退出(EXIT _ SUCCESS);
}
函数set_semvalue使用SETVAL命令来初始化semctl调用中的信号量。在我们使用信号量之前,我们需要这样做。
静态int set_semvalue(void)
{
union semun sem _ union
SEM _ union . val=1;
if(semctl(sem_id,0,SETVAL,SEM _ union)=-1)返回0;
返回1;
}
del _ semvalue函数具有几乎相同的格式,只是semctl调用使用IPC_RMID命令来删除信号量ID:
静态void del_semvalue(void)
{
union semun sem _ union
if(semctl(sem_id,0,IPC_RMID,SEM _ union)=-1)
fprintf(stderr,未能删除信号量/n );
}
6 semaphore_p函数将信号量减1(等待):
静态int信号量_p(void)
{
结构SEM buf 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 );
返回0;
}
返回1;
}
7 semaphore_v函数将sembuf结构的sem_op部分设置为1,这样信号量就可用了。
静态int信号量_v(void)
{
结构SEM buf 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 );
返回0;
}
返回1;
}
注意,这个简单的程序每个程序只有一个二进制信号量,尽管如果我们需要多个信号量,我们可以扩展这个程序来传递多个信号量变量。通常,一个简单的二元信号量就足够了。
我们可以通过多次调用这个程序来测试我们的程序。第一次,我们传递一个参数来通知程序它不负责创建和删除信号量。另一个调用没有传递参数。
以下是这两个调用的输出示例:
$ ./sem1 1
[1] 1082
$ ./sem1
ooxxooxooxxooxxooxoxxooooxxooooxxooxoxxooxoxxxx
1083 -完成
1082 -完成
$
正如我们所看到的,O和X成对出现,表明临界区已经被正确处理。如果这个程序在我们的系统上不能正常运行,也许我们需要在调用程序之前使用命令stty -tostop来确保生成tty输出的后台程序不会导致信号生成。
这个程序首先选择通过使用semget函数生成信号量ID而获得的密钥。IPC_CREAT标志导致在必要时创建一个信号量。
如果这个程序有参数,它负责通过使用我们的set_semvalue函数来初始化信号量,这个函数是更通用的semctl函数的简化接口。同时,他还使用提供的参数来决定输出哪个字符。睡眠只是让我们有时间在这个程序被多次执行之前调用它的另一个副本。在程序中,我们使用srand和rand来引入一些伪随机计数。
这个程序循环十次,在其关键和非关键区域等待一段随机的时间。通过调用semaphore_p和semaphore_v函数来保护临界区代码,这是更通用的semop函数的简化接口。
在删除信号量之前,用参数调用的程序副本将等待其他调用结束。如果信号量没有被删除,它将继续存在于系统中,尽管不再有程序使用它。在实际过程中,确保我们没有遗留信号是非常重要的。当我们下次运行程序时,遗留的信号量会引起问题,而信号量是受限资源,所以我们必须小心使用。