进程间通信概述
1. 进程通信机制
一般情况下,系统中运行着大量的进程,而每个进程之间并不是相互独立的,有些进程之间经常需要互相传递消息。但是每个进程在系统中都有自己的地址空间,操作系统通过页表和实际物理内存所关联,不允许其他进程随意进入。因此,就必须有一种机制既能保证进程之间的通信,又能保证系统的安全,即进程间通信机制——IPC (Inter-Process Communication)。
Linux中的内存空间分为系统空间和用户空间。在系统空间中,由于各个线程的地址空间都是共享的,即一个线程能够随意访问kernel中的任意地址,所以无需进程通信机制的保护。而在用户空间中,每个进程都有自己的地址空间,一个进程为了与其他进程通信,必须陷入到有足够权限访问其他进程空间的kernel中,从而与其他进程进行通信。在Linux中支持System V 进程通信的手段有三种:消息队列(Message queue)、信号量(Semaphore)、共享内存(Shared memory)。
2. 进程通信对象标示符和键
在kernel中,对每一类I P C 对象,都由一个非负整数来索引。为了识别并唯一标识各个进程通信的对象,需要一个标识符(即IPC标示符)来标识各个通信对象。而为了获取一个独一无二的通信对象,必须使用键(可使用ftok( )函数生成,返回值key)。这里的键是用来定位I P C 对象的标识符的。
背景知识
1. 原子操作(atomic operation)
原子操作意为不可被中断的一个或一系列操作,也可以理解为就是一件事情要么做了,要么没做。而原子操作的实现,一般是依靠硬件来实现的。
2. 同步与互斥
同步:在访问资源的时候,以某种特定顺序的方式去访问资源
互斥:一个资源每次只能被一个进程所访问。
同步与互斥是保证在高效率运行的同时,可以正确运行。大部分情况下同步是在互斥的基础上进行的。
3. 临界资源
不同进程能够看到的一份公共的资源(如:打印机,磁带机等),且一次仅允许一个进程使用的资源称为临界资源。
4. 临界区
临界区是一段代码,在这段代码中进程将访问临界资源(例如:公用的设备或是存储器),当有进程进入临界区时,其他进程必须等待,有一些同步的机制必须在临界区段的进入点和离开点实现,确保这些共用资源被互斥所获得。
5. 相关命令
- ipcs -s 显示已存在的信号量
- ipcrm -s 删除指定信号量
注意:有时候因为权限问题需要在root下查看与删除。
什么是信号量(Semaphore)
信号量(Semaphore)可以被看做是一种具有原子操作的计数器,它控制多个进程对共享资源的访问,通常描述临界资源当中,临界资源的数目,常常被当做锁(lock)来使用,防止一个进程访问另外一个进程正在使用的资源。
信号量本身不具有数据交换的功能,而是控制其他资源来实现进程间通信,在此过程中负责数据操作操作的互斥、同步等功能。
简言之:信号量的主要目的是为了保护临界资源。
1. 为什么要使用信号量
为了防止出现因多个进程同时访问一个共享资源而引发的问题,我们需要一种方法,可以通过生成并使用令牌来授权,在任一时刻只能有一个执行流访问代码的临界区域。而信号量就可以提供这样的一种访问机制,让一个临界区同一时刻只有一个执行流在访问它。
2. 信号量的工作原理
-
测试控制该资源的信号量。
-
若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,,表示一个资源被使用。
-
若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0,进程被唤醒,从新进入第1步。
-
当进程不再使用由一个信号控制的共享资源时,该信号量值增1,如果有进程正在休眠等待该信号量,则会被唤醒。
为了正确地实现信号量,信号量的操作应是原子操作,所以信号量通常是在内核中实现的。
- 3. Linux的信号量机制
-
在System V中信号量并非是单个非负值,而必须将信号量定义为含有一个或多个信号量值的集合。当创建一个信号量时,要指定该集合中信号量值的数量。
-
创建信号量(semget)和对信号量赋初值(semctl)分开进行,这是一个弱点,因为不能原子地创建一个信号量集合,并且对该集合中各个信号量赋初值。
-
即使没有进程在使用I P C资源,它们仍然是存在的,要时刻防止资源被锁定,避免程序在异常情况下结束时没有解锁资源,可以使用关键字(SEM_UNDO )在退出时恢复信号量值为初始值。
相关函数
1、ftok函数
#include <sys/ipc.h>
#include <sys/types.h>
key_t ftok(const char* path, int id);
-
ftok 函数把一个已存在的路径名和一个整数标识转换成一个key_t值,即IPC关键字
-
path 参数就是你指定的文件名(已经存在的文件名),一般使用当前目录。当产生键时,只使用id参数的低8位。
-
id 是子序号, 只使用8bit (1-255)
-
返回值:若成功返回键值,若出错返回(key_t)-1
在一般的UNIX实现中,是将文件的索引节点号取出(inode),前面加上子序号的到key_t的返回值
2、semget函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget( key_t key, int nsems, int semflg);
-
用来创建一个信号集,或者获取已存在的信号集。
-
key: 所创建或打开信号量集的键值(ftok成果执行的返回值)。
-
nsems:创建的信号量集中的信号量个数,该参数只在创建信号量时有效。
-
semflg :调用函数的操作类型,也可用于设置信号量集的访问权限,通过or运算使用。
- IPC_CREAT | IPC _EXCL | 0666 :一般用于创建,可保证返回一个新的ID,同时制定权限为666
- IPC_CREAT : 用于获取一个已经存在的ID
-
返回值:成功返回信号量集的标识符,失败返回-1,errno被设置成以下的某个值:
- EACESS : 没有访问该信号量集的权限。
- EEXIST:信号量集已经存在,无法创建。
- EINVAL:参数nsems的值小于0,或者大于该信号量集的限制,或者是该key关联的信号量以存在,并且nsems的值大于该信号量集的信号量数。
- ENOENT:信号量集不存在,同时没有使用,IPC_CREAT。
- ENOMEM:没有足够的内存创建新的信号量集。
3、semctl函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semun, int cmd, ...);
-
用来初始化信号集,或者删除信号集。
-
semid:信号量集I P C 标识符。
-
semun:操作信号在信号集中的编号,第一个信号的号是0.
-
cmd:在semid指定的信号量集合上执行此命令。
-
第四个参数是可选的,如果使用该参数,则其类型是semun,它是多个特定命令参数的联合(union):
union semun
{
int val;
struct semid_ds * buf;
unsigned short * array;
struct seminfo * __buf;
};
-
第三个参数cmd常用命令:
- IPC_SEAT:对此集合取semid_ds 结构,并存放在由arg.buf指向的结构中。
- IPC_RMID:从系统中删除该信号量集合。
- SETVAL:设置信号量集中的一个单独的信号量的值,此时需要传入第四个参数。
-
返回值:成功返回一个正数,失败返回-1。
4、 semop函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf * sops, unsigned nsops);
-
功能:操作一个或一组信号。也可以叫PV操作
-
semid:信号集的识别码,可以通过semget获取。
-
sops:是一个指针,指向一个信号量操作数组。信号量操作由结构体sembuf 结构表示如下:
struct sembuf
{
unsigned short sem_num; // 在信号集中的编码 0 , 1, ... nsems-1
short sem_op; //操作 负值或正值
short sem_flg; // IPC_NOWAIT, SEM_UNDO
};
-
sembuf结构体参数说明:
-
sem_num:操作信号在信号集中的编号,第一个信号的编号是0,最后一个信号的编号是nsems-1。
-
sem_op:操作信号量
-
若sem_op 为负(P操作), 其绝对值又大于信号的现有值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权。
-
若sem_op 为正(V操作), 该值会加到现有的信号内值上。通常用于释放所控制资源的使用权。
- sem_op的值为0:如果没有设置IPC_NOWAIT,则调用该操作的进程或线程将暂时睡眠,直到信号量的值为0;否则进程或线程会返回错误EAGAIN。
-
-
sem_flg: 信号操作标识,有如下两种选择:
-
IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
-
SEM_UNDO:程序结束时(正常退出或异常终止),保证信号值会被重设为semop()调用前的值。避免程序在异常情况下结束时未解锁锁定的资源,早成资源被永远锁定。造成死锁。
-
-
-
nsops:信号操作结构的数量,恒大于或等于1.
-
返回值:成功执行时,都会回0,失败返回-1,并设置errno错误信息。
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sem.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
static int sem_id = 0;
static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();
int main(int argc, char *argv[])
{
char message = 'X';
int i = 0;
//创建信号量
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);
}
//设置要输出到屏幕中的信息,即其参数的第一个字符
message = argv[1][0];
sleep(2);
}
for(i = 0; i < 10; ++i)
{
//进入临界区
if(!semaphore_p())
exit(EXIT_FAILURE);
//向屏幕中输出数据
printf("%c", message);
//清理缓冲区,然后休眠随机时间
fflush(stdout);
//离开临界区,休眠随机时间后继续循环
if(!semaphore_v())
exit(EXIT_FAILURE);
sleep(2);
}
sleep(3);
printf("\n%d - finished\n", getpid());
if(argc > 1)
{
//如果程序是第一次被调用,则在退出前删除信号量
sleep(3);
del_semvalue();
}
exit(EXIT_SUCCESS);
}
static int set_semvalue()
{
//用于初始化信号量,在sem_union的val字段中设置信号量初值。使用信号量之前必须先初始化!
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()
{
//删除信号量
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
printf("Failed to delete semaphore\n");
}
static int semaphore_p()
{
//对信号量做减1操作,即加锁 P(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1; //P()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
printf("semaphore_p failed\n");
return 0;
}
return 1;
}
static int semaphore_v()
{
//这是一个释放操作,它使信号量变为可用,即解锁 V(sv)
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = 1; //V()
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == -1)
{
printf("semaphore_v failed\n");
return 0;
}
return 1;
}