Linux进程间通信——Systme V版
1 System V与POSIX
简单的来说System V 其实就是Unix操作系统众多版本中的一支。
POSIX:可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),是一个电气与电子工程学会即IEEE开发的一系列 标准,目的是为运行在不同操作系统的应用程序提供统一的接口,实现者是不同的操作系统内核。
在System V中,共享内存、信号量和消息队列通常被称为 IPC 对象。
1.1 IPC 标识符
在 Linux 系统中,标识符是一个整数,每一个 IPC 对象的标识符在系统内部都是唯一的。在系统内部, 通过传递 IPC 对象的标识符可以访问该对象。
1.2 IPC 键值
键值是一个 IPC 对象的 外部标识,可以由程序员自己指定,或者使用ftok()函数获得,它主要用于多个进程都访问一个特定的 IPC对象的情况。
1.2.1 键值和进程对IPC对象访问权限
在创建一个 IPC 对象时,需要指定一个键值。如果该IPC 键是公用的 ,那么系统中所有进程通过权限检查后都可以访问到相应的 IPC 对象;如果该键是私有的 ,那么键值通常定义为 0(宏名为IPC_PRIVATE),只能适用于有血缘关系间的进程通信。
1.2.2 使用函数获取IPC键值
获取键值函数ftok()是根据文件的inode和指定的proj_id参数组合产生键值。可以在终端使用ls -i命令查看文件的inode值。
key_t ftok(const char *pathname, int proj_id)
参数
pathname 必须是存在的文件或目录
proj_id 要指定的项目id
返回值
成功,key_t类型的键值
失败,-1,并设置errno
1.3 IPC 对象属性
在Linux内核中,IPC对象除了有标识该对象的唯一标识符和外部键(key)之外,还有这个对象的一些属性,如该对象的所有者或者访问权限等信息,这些属性都定义在ipc_perm 结构体中。(内核源码中include/linux/ipc.h)源码如下:
struct kern_ipc_perm {
spinlock_t lock;
bool deleted;
int id;
key_t key; //键值,
kuid_t uid; //对象拥有者对应进程的有效用户识别号和有效组识别号
kgid_t gid;
kuid_t cuid; //对象创建者对应进程的有效用户识别号和有效组识别号
kgid_t cgid;
umode_t mode; //存取模式
unsigned long seq; //序列号
void *security;
struct rhash_head khtnode;
struct rcu_head rcu;
refcount_t refcount;
} ____cacheline_aligned_in_smp __randomize_layout;
1.4 IPC命令
1.4.1 常用的IPC命令
使用shell命令ipcs 主要是查看IPC对象的信息
ipcs -s /*查看信号量的信息*/
ipcs -m /*查看共享内存的信息*/
ipcs -q /*查看消息队列的信息*/
ipcs /*查看所有IPC对象的信息*/
使用shell命令ipcrm 用来删除IPC对象
ipcrm –m shmid /*删除标识符为 shmid 的共享内存信息*/
ipcrm –q msqid /*删除标识符为 msqid 的消息队列信息*/
ipcrm –s semid /*删除标识符为 semid 的信号量信息*/
ipcrm –M shmkey /*删除键值为 shmkey 的共享内存信息*/
ipcrm –Q mskey /*删除键值为 mskey 的消息队列信息*/
ipcrm –S semkey /*删除键值为 semkey 的信号量信息*/
2 共享内存
共享内存就是通过两个或者多个进程共享同一块内存区域来实现进程间的通信。存放在共享内存中的数据是任何进程都可以对其进行读取的。
共享内存进程间通讯特点如下:
优点:共享内存所实现的进程间通信是最快速的
缺点:多个进程同时读写某一块共享内存时,会造成共享内存中数据的混乱
每一个共享内存的对象都有其指定的定义类型,该结构体类型为 shmid_ds,定义形式如下:
struct shmid_ds
{
struct ipc_perm shm_perm; /*共享内存的 ipc_perm结构对象*/
int shm_segsz; /*共享内存区域字节大小*/
ushort shm_lkcnt; /*共享内存区域被锁定的时间数*/
pid_t shm_cpid; /*创建该共享内存的进程 ID*/
pid_t shm_lpid; /*最近一次调用 shmop()函数的进程 ID*/
ulong shm_nattch; /*使用该共享内存的进程数*/
time_t shm_atime; /*最近一次附加操作的时间*/
time_t shm_dtime; /*最近一次分离操作的时间*/
time_t shm_ctime; /*最近一次改变的时间*/
};
2.1 创建共享内存函数
在使用共享内存实现进程间通信时,需要首先调用 shmget()函数创建一块共享内存区域,如果已经存在一块共享内存区域,那么该函数可以打开这个已经存在的共享内存。
该函数的定义形式如下:
int shmget(key_t key,size_t size,int shmflg);
参数
key:共享内存的键值。
size:表示新创建的共享内存区域的大小,以字节表示。
shmflg:用于设置共享内存的访问权限
返回值
成功,返回共享内存区域的标识符
失败,-1,并设置errno值
shmget()函数的功能与参数的不同搭配有关:
当 key 为 IPC_PRIVATE, shmflg 为 IPC_CREAT, 函数功能创建一个新的共享内存区域
使用IPC_PRIVATE创建的IPC对象, key值属性为0,和IPC对象的标识符就没有了对应关系。这样无血缘关系的进程,就不能通过key值来得到IPC对象的编号(因为这种方式创建的IPC对象的key值都是0)。因此,这种方式产生的IPC对象,和无名管道类似,不能用于无血缘关系进程间通信。
key 值不为 IPC_PRIVATE, shmflg 为 IPC_CREAT,并且已经存在一个与 key值相对应的共享内存区域,那么该函数调用会失败
2.2 共享内存映射函数
shmat()函数的功能是将共享内存区域附加到指定进程的地址空间中,该函数的定义形式如下:
void *shmat(int shmid,const void *shmaddr,int shmflg);
参数
shmid:共享内存的标识符。
shmaddr:指定进程的内存地址
shmaddr 为 NULL,系统会自动选择一个合适的内存地址,将共享内存区域附加到此地址上。
shmaddr 不为 NULL,并且 shmflg 参数指定了 SHM_RND 值时,函数会将共享内存区域
附加到(shmaddr-(add mod SHMLBA))计算所得的地址中。
SHM_RND 表示取整, SHMLBA 表示低边界地址的整数倍
shmflg:表示该函数的操作方式,一般为0
返回值
成功,返回指向该共享内存区域的指针
失败,返回-1,并设置errno值。
2.3 共享内存操作函数
shmctl()函数主要实现了对共享内存区域的多种控制操作,该函数的定义形式如下:
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
参数
shmid 共享内存标识符
cmd 在 Linux 系统中,有如下 8 种控制信息。
IPC_STAT:在内核中,将与标识符 shmid 相关的共享内存的数据复制到 buf 指向的共享内存
区域中。
IPC_SET:根据参数 buf 指向的 shmid_ds 结构中的值设置 shmid 标识符所指的共享内存的相
关属性。
IPC_RMID:删除 shmid 标识符所指的共享内存区域。
IPC_INFO:该值为 Linux 系统中特有的参数值,用于获取关于系统共享内存限制和 buf 指向
的相关参数的信息。
SHM_STAT:该值为 Linux 系统中特有的参数值,功能与 IPC_STAT 相同,但是参数 shmid
在这里不代表共享内存的标识符,而是一个内核中维持所有共享内存区域信息的数组的索引值。
SHM_LOCK:该值为 Linux 系统中特有的参数值,用于阻止共享内存区域的交换。
SHM_UNLOCK:该值为 Linux 系统中特有的参数值,用于解锁共享内存区域。
2.4 进程地址分离共享内存函数
shmdt()函数的功能是当某一进程不再使用该内存区域时,将shmat()函数附加的共享内存区域从该进程的地址空间中分离出来,此共享内存区域仍然存在。该函数的定义形式如下:
int shmdt(const void *shmaddr);
参数
shmaddr 为shmat()函数成功时返回的地址指针。
返回值
成功时,返回0
失败,返回-1,并设置errno值
2.5 demo演示
实现子进程向创建的共享内存,写入数据,父进程读数据的实验。
share_mem.c代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
int main()
{
int shmid;
int proj_id;
key_t key;
int size;
char *addr;
pid_t pid;
key=IPC_PRIVATE;
//创建共享内存
shmid=shmget(key,1024,IPC_CREAT|0660);
if(shmid==-1)
{
perror("create share memory failed!");
return 1;
}
//将共享内存添加到进程地址上
addr=(char *)shmat(shmid,NULL,0);
if(addr==(char *)(-1))
{
perror("cannot attach!");
return 1;
}
//打印共享内存地址
printf("share memory segment's address:%#x\n",addr);
//向共享内存里写入数据
strcpy(addr,"This is shared memory!");
//创建子进程
pid=fork();
if(pid==-1)
{
perror("error!!!!");
return 1;
}
else if(pid==0)
{
printf("child process string is: %s\n",addr);
exit(0);
}
else
{
//因为是阻塞等待回收子进程,所以才能达到子进程先读,父进程再读的效果。
wait(NULL);
printf("parent process string is: %s\n",addr);
//将共享内存从线程地址中分离
if(shmdt(addr)==-1)
{
perror("release failed!");
return 1;
}
//删除共享内存
if(shmctl(shmid,IPC_RMID,NULL)==-1)
{
perror("failed!");
return 1;
}
}
return 0;
}
2.5.1 demo实验现象
运行程序,子进程先打印,父进程再打印,结果如下:
3 信号量
信号量解决同步、互斥问题。
同步:使进程按照先后顺序访问临界资源
互斥:一个进程进入临界区访问临界资源,那么剩下的进程就需要进行等待,只有当它退出临界区,才允许下一个进程访问。
3.1 信号量的工作原理
信号量的工作原理是:当有一个进程要求使用某一共享内存中的资源时,系统会首先判断该资源的信号量,也就是统计可以访问该资源的单元个数。如果系统判断出该资源信号量值大于 0,进程就可以使用该资源,并且信号量要减 1,当不再使用该资源时,信号量再加 1,方便其他用户使用时,系统对其进行准确的判断。如果该资源的信号量等于 0,进程会进入休眠状态,等候该资源有人使用结束, 信号量大于 0,这样进程就会被唤醒,对该资源进行访问。
3.2 创建信号量函数
在使用信号量控制进程间同步时,需要首先创建一个信号量集, semget()函数实现了创建一个新的信号量集操作和打开一个已经存在的信号量集的操作,该函数的定义形式如下:
int semget(key_t key,int nsems,int semflg);
参数
key 表示所创建的信号量集的键值。
nsems 表示信号量集中信号量的个数。当semget()函数实现的作用是创建一个新的信号量集时,该参数才有效。
semflg 用于设置信号量集的访问权限,也可以表示该函数的操作类型。
返回值
成功,返回值为与参数key相关联的标识符
失败,返回-1,并设置errno
3.3 信号量集操作函数
semop()函数实现的功能是对信号量集中的信号量进行操作。具体的操作内容与该函数的参数的设定有关,该函数的定义形式如下:
int semop(int semid,struct sembuf*sops,unsigned nsops)
参数
semid:表示要进行操作的信号量集的标识符。
sops:为 sembuf 结构体指针变量, semop()函数通过此参数指定对单个信号量的操作行为。
nsops:代表要操作的sops的数量
参数sops 的数据类型为 sembuf 结构体,如下
struct sembuf
{
unsigned short sem_num; /*信号量值*/
short sem_op; /*信号的操作*/
short sem_flg; /*操作标识*/
}
结构体成员参数说明
sem_op 变量值根据其取值的范围确定执行的操作行为
大于 0,则需要释放掉资源;
若小于 0,则要获取共享资源;
若为 0,则表示资源都已经处于使用状态。
sem_flg 变量值作为操作的标识,与此函数相关的标识有 IPC_NOWAIT 和 SET_UNDO。
3.4 信号量集的控制函数
对信号量集的控制主要通过 semctl()函数实现。例如,通常在使用信号量集时,都要对信号量集中的元素进行初始化, semctl()控制函数就可以实现此功能。该函数的原型为:
int semctl(int semid,int semnum,int cmd,…/* union semun arg */);
参数
semid 要修改的信号量集的标识符;
semnum 表示需要修改的信号量集中的信号量个数;semnum值在0和nsems-1之间,包括0和nsems-1。
cmd 表示该函数的控制类型。
如果参数 cmd 的取值为以下几种形式,则返回值为指定的信息。
SETVAL 设置成员semnum的semval值。该值由arg.val指定。
GETPID 返回值为 sempid 的取值。
GETVAL 返回成员semnum的semval值。
GETZCNT 返回值为 semzcnt 的取值。
IPC_INFO 返回值为内核中信号量集数组的最高索引值。
IPC_RMID 从系统中删除该信号量集合。
SEM_INFO 与IPC_INFO相同。
SEM_STAT 返回值为 semid 指定的标识符。
参数arg与参数 cmd 有关,是一个共用体类型参数,用于读取或存储函数返回的结果。
semun共用体的定义形式如下:
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
返回值
成功,返回值为非负整数,与cmd参数的设置有关
失败,返回值为-1,并设置errno
3.5 demo演示
在 sl1.c 文件中创建信号量集,模拟系统分配资源,假设系统中总共有 4 个资源可以使用,每隔 3秒就有一个资源会被占用。程序的代码如下:
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/sem.h>
#include <unistd.h>
#define RESOURCE 4
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int main(void)
{
key_t key;
int semid;
struct sembuf sbuf ={0,-1,IPC_NOWAIT};
union semun arg;
//获取键值
if((key=ftok("./text.txt",2))==-1)
{
perror("ftok error!\n");
exit(1);
}
//创建信号量集,返回的是与键值(key)关联的信号量集标识符semid
if((semid = semget(key,1,IPC_CREAT|0666)) == -1)
{
perror("semget error!\n");
exit(1);
}
arg.val=RESOURCE;
printf("可使用的资源共有 %d 个!\n",arg.val);
//设置,第二个参数0表示第0个信号量
if (semctl(semid, 0, SETVAL, arg) == -1)
{
perror("semctl error!\n");
exit(1);
}
while(1)
{ //获取共享资源,需要设置sbuf的成员
if(semop(semid,&sbuf,1) == -1)
{
perror("semop error!\n");
exit(1);
}
sleep(3);
}
//删除信号量集合,
semctl(semid,0,IPC_RMID,0);
exit(0);
}
在 sl2.c 文件中,根据 semop()函数检测是否有资源可以利用,并且返回可使用资源的个数。程序的代码如下:
#include <sys/types.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int main(void)
{
key_t key;
int semid,semval;
union semun arg;
//获取键值
if((key=ftok("./text.txt",2))==-1)
{
perror("ftok error!\n");
exit(1);
}
//创建信号量集,返回的是与键值(key)关联的信号量集标识符semid
if((semid = semget(key,1,IPC_CREAT|0666)) == -1)
{
perror("semget error!\n");
exit(1);
}
while(1)
{ //返回成员semnum的semval(信号量)值。
if((semval = semctl(semid,0,GETVAL,0)) == -1)
{
perror("semctl error!\n");
exit(1);
}
if(semval > 0)
{
printf("还有 %d 个资源可以使用!\n",semval);
}
else
{
printf("没有资源可以使用!\n");
break;
}
sleep(3);
}
exit(0);
}
3.5.1 demo实验现象
分别编译sl1.c和sl2.x代码,先执行sl1程序,再执行sl2程序。
sl2程序每隔3s打印可用资源数量。
当sl2程序,打印可用资源数量,变为0的时候,在过3s,sl1程序执行结束。
4 消息队列
4.1 消息队列的概述
消息队列是消息的链表,存在内存中,由内核维护消息队列的特点。
1、消息队列中的消息是有类型和格式。
2、消息队列可以实现消息的随机查询。消息不一定要以先进先出的次序取出,编程时可以按消息的类型读取。
3、消息队列允许一个或多个进程向它写入或者读取消息。
4、与无名管道、命名管道一样,从消息队列中读出消息,消息队列中对应的数据会被删除。
5、每个消息队列都有消息队列标识符,消息队列的标识符在整个系统中是唯一的。
6、只有内核重启或人工删除消息队列时,该消息队列才会被删除。若不人工删除消息队列,消息队列会一直存在系统中。
4.2 消息队列的格式定义
struct msgbuf
{
long mtype; /*消息类型 必须是第一个成员且必须是long型*/
char mtext[100]; /*消息正文 */
/*消息的正文可以有多个成员*/
};
4.3 创建消息队列函数
msgget()函数,创建一个新的或打开一个已经存在的消息队列。不同的进程调用此函数,只要用相同的key值就能得到同一个消息队列的标识符。
int msgget(key_t key,int msgflag)
参数
key IPC键值
msgflag 标识函数的行为及消息队列的权限
IPC_CREAT 创建消息队列
IPC_EXCL 检测消息队列是否存在
返回值
成功,消息队列的标识符
失败,返回-1,并设置errno值
4.4 发送消息
将消息添加到消息队列中
int msgsnd(int msqid, const void *msgp,size_t msgsz,int msgflag)
参数
msgqid 消息队列的标识符
msgp 待发送消息结构体的地址
msfsz 消息正文的字节数
msgflag 函数的控制属性
0 当消息队列中空间被占满时,msgsnd调用阻塞直到条件满足为止。
IPC_NOWAIT 若消息没有立即发送则调用该函数的进程会立即返回
返回值
成功,0
失败,-1
4.5 接收消息
从标识符为msqid的消息队列中接收一个消息。一旦消息成功,则消息在消息队列中被删除。
ssize_t msgrcv(int msqid,void *msgp,size_t msgsz,long msgtype,int msgflg)
参数
msqid 消息队列的标识符,代表要从哪个消息队列中获取消息。
msgp 存放消息结构体的地址
msgsz 消息正文的字节数
msgtyp 消息的类型
msgtyp=0 返回队列中的第一个消息
msgtyp>0 返回队列中消息类型为msgtyp的消息
msgtyp<0 返回队列中消息类型值小于或等于msgtyp绝对值的消息,若这种消息有多个,则取类型值最小的消息
msgflg 函数的控制属性
0 msgrcv调用阻塞直到接收消息成功为止
MSG_NOERROR 若返回的消息字节数大于nbytes值,则消息被截断到nbytes字节,且不通知消息发送进程。
IPC_NOWAIT 调用进程会立即返回。若没有收到消息则立即返回-1。
返回值
成功,返回接收到的字节数
失败,-1
若消息队列中有多种类型的消息,msgrcv获取消息的时候按消息类型获取,不是先进先出的。在获取某类型消息的时候,若队列中有多条此类型的消息,则获取先添加的消息,即先进先出。
4.6 消息队列的控制
可以对消息队列进行各种控制,如修改消息队列的属性,或删除消息队列。
int msgctl(int msqid,int cmd,struct msqid_ds *buf)
参数
msqid 消息队列的标识符
cmd 函数功能的控制
IPC_RMID 删除由msqid指示的消息队列,将它从系统中删除并破坏相关数据结构。
IPC_STAT 将msqid相关的数据结构中各个元素的当前值传入到由buf指向的结构中。
IPC_SET 将msqid相关的数据结构中的元素设置为由buf指向的结构中的对应值。
buf 用来存放或更改消息队列的属性
返回值
成功,返回0
失败,返回-1
4.7 demo实验
发送消息代码,send_msg.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//定义一个消息类型结构体
typedef struct msgbuf
{
long mtype;
char mtext[64]; //消息正文
char name[32]; //发送者的姓名
}MSG;
int main(int argc,const char *argv[])
{
//获取IPC的唯一KEY值
key_t key = ftok("/",2021);
printf("key=%#x\n",key);
//创建一个消息队列
int msg_id = msgget(key,IPC_CREAT|0666);
printf("msg_id=%d\n",msg_id);
MSG msg;
memset(&msg,0,sizeof(msg));
//设置消息类型
msg.mtype = 20;
//设置要发送的内容
strcpy(msg.name,"I'm bob");
strcpy(msg.mtext,"hello msg");
//发送消息
msgsnd(msg_id,&msg,sizeof(MSG)-sizeof(long),0);
return 0;
}
接收消息代码,recv_msg.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//定义一个消息类型结构体
typedef struct msgbuf
{
long mtype; //消息类型
char mtext[64]; //消息正文
char name[32]; //发送者的姓名
}MSG;
int main(int argc,const char *argv[])
{
//获取IPC的key值,ftok参数要与发送端中ftok函数一致,确保获得同一key值
key_t key = ftok("/",2021);
printf("key=%#x\n",key);
//创建一个消息队列,这里这个应该是获取与key绑定的标识符
int msg_id = msgget(key,IPC_CREAT|0666);
printf("msg_id=%d\n",msg_id);
//接收消息
MSG msg;
memset(&msg,0,sizeof(msg));
//接收消息类型为20的消息
msgrcv(msg_id,&msg,sizeof(MSG)-sizeof(long),20,0);
//打印消息内容
printf("发送者:%s\n",msg.name);
printf("消息:%s\n",msg.mtext);
return 0;
}
4.7.1 demo现象
先运行send_msg程序发送消息,再运行recv_msg程序,成功收到消息,终端打印的结果如下: