Linux系统编程——进程间通信教程
文章目录
前言
本篇是Linux系统编程系列的第三篇——进程间通信,介绍Linux中进程间通信的几种方式,本篇的学习重点在于熟悉每种进程间通信方式的特点并能熟练使用。这是本篇的1.0版本,慢慢会补充一些代码例子加深理解,例子会涉及到前几篇的知识,在掌握进程间通信的同时也是对之前的知识的巩固。
- -
概述
进程间通信的概念
进程间通信指的就是用户空间里的进程之间相互通信。我们知道,用户空间的进程之间相互独立、互不干扰。在用户空间中的进程好比没有门窗的房子,互相之间是无法直接通信的。那么,进程之间是如何实现通信的呢?答案是通过内核实现进程间通信。在内核中可以创建一个对象,用户空间的进程以这个对象为媒介进行数据交换,达到进程间通信的目的。
进程间通信的方式
进程间通信的方式有从早期的UNIX继承过来的进程间通信、System V 进程间通信、socket。其中,早期的UNIX进程间通信分为无名管道、有名管道、信号。System V进程间通信分为共享内存、消息队列、信号灯。socket在本地通信中不大使用,多用于网络通信,因此socket编程会在网络编程中介绍。
进程间通信的框架思路
Linux中“一切皆文件”,进程间通信的学习也可以借助文件IO的思想。下文介绍各种进程间通信方式时也会采用这个框架思路,以便大家有个清晰的认识。
- 创建:创建或打开通信对象。
- 写入:向进程通信对象写入内容。
- 读取:从进程通信对象中读取内容。
- 关闭:关闭或删除进程通信对象。
- -
早期的UNIX进程间通信
无名管道
基本概念
无名管道是进程间通信的一种方式,具有以下的特点:
- 只能用于具有亲缘关系的进程之间的通信。因为无名管道创建后在文件系统中是没有文件的,只存在于内核的内存空间中,所以只能通过继承的方式获得无名管道。
- 半双工的通信模式,具有固定的读端和写端。
- 管道是一个顺序队列,管道内数据读出后就不存在了。
无名管道创建
说明
调用pipe函数可以创建无名管道,无名管道创建成功时会得到两个文件描述符,fd[0]和fd[1],fd[0]固定用于读端,fd[1]固定用于写端。
pipe函数
函数说明
创建管道
函数原型
int pipe(int fd[2])
传入参数
fd[2]:无名管道的两个文件描述符,fd[0]固定用于读端,fd[1]固定用于写端。
返回值
成功返回0,出错返回-1。
无名管道关闭
调用close函数可以关闭无名管道。当需要关闭管道时,只需要将这对读写文件描述符关闭即可。管道关闭后,管道的内容也会被释放。
无名管道读写
无名管道的读写可以通过调用read函数和write函数实现。调用pipe函数创建管道后,再调用fork创建子进程,子进程复制了父进程创建的管道,此时调用close函数关闭子进程的读端,关闭父进程的写端,这样就建立了一条子进程写父进程读的通道。相反,关闭子进程的写端,父进程的读端,就可以建立父进程写子进程读的通道。
读管道时具有以下情况:
- 写端存在:至少有一个进程通过管道描述符写管道称为写端存在。管道内有数据时,read函数返回实际字节数,管道内无数据时,进程阻塞,直到管道内有数据。
- 写段不存在:管道内有数据时,read函数是实际读取的字节数,管道内无数据时,read函数返回0。
写管道时具有以下情况:
- 读端存在:至少有一个进程通过管道描述符读管道称为读端存在。管道内有空间时,write函数返回实际写入字节数。管道内空间不足时,写满空间后进程写阻塞,直到读端开始读数据,管道内空间有足够空间写入剩余数据为止。
- 读端不存在:管道断裂,进程收到SIGPIPE信号,默认操作是关闭进程。
例子
父进程作为写端,子进程作为读端,父进程发送“hello linux”,子进程收到后打印到终端上。
/* pipe.c */
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
int fd[2],ret,readnum,writenum ;
int *stutus;
pid_t pid;
char raedbuff[256];
const char writebuff[] = "hello linux";
/* 创建管道 */
if (pipe(fd) < 0)//创建管道出错
{
printf("pipe create error\n");
exit(1);
}
/* 创建子进程 */
pid = fork();
if(pid < 0)//创建子进程出错
{
printf("fork error\n");//fork出错
}
else if(pid == 0)//子进程
{
/* 关闭子进程写端 */
if (close(fd[1]) < 0)//关闭出错
{
printf("child process close pipe error\n");
exit(1);
}
/* 等待父进程关闭读端 */
sleep(3);
/* 清除读缓存,子进程读取管道内容 */
memset((void*)raedbuff, 0, sizeof(raedbuff));
if ((readnum = read(fd[0], raedbuff, 256)) > 0)
{
printf("%d bytes read from the pipe is %s\n",readnum ,raedbuff);
}
/* 关闭子进程读描述符 */
close(fd[0]);
exit(0);
}
else//父进程
{
/* 关闭父进程读端 */
if (close(fd[0]) < 0)//关闭出错
{
printf("father process close pipe error\n");
exit(1);
}
/* 等待子进程关闭写端 */
sleep(3);
/* 父进程写管道 */
if((writenum = write(fd[1], writebuff, strlen(writebuff))) != -1)
{
printf("father wrote %d bytes : %s\n",writenum,writebuff);
}
/* 关闭父进程写描述符 */
close(fd[1]);
/* 等待子进程退出 */
wait(stutus);
exit(0);
}
}
运行结果:
有名管道
基本概念
无名管道只能用于具有亲缘关系的进程之间的通信,而有名管道恰好突破了这个限制。与无名管道不同的是,有名管道创建成功后会在文件系统中生成文件类型为p的文件,称为管道文件。管道文件生成后,两个进程就可以像操作普通文件一样打开、关闭、读写管道文件了。有名管道需要注意的几个特点:
- 有名管道和无名管道一样是一个顺序队列,采用先进先出的机制,因此不能像普通文件一样调用lseek函数设置读写指针。
- 有名管道的内容存在于内核的内存中,文件系统的管道文件大小始终是0,关闭管道文件后,管道内容也会被释放。
- 管道内数据读出后就不存在了。
有名管道的创建
说明
调用mkfifo函数可以创建有名管道。调用mkfifo后仅仅是在文件系统中创建有名管道的文件节点,并没有在内核空间创建管道,只有调用open函数打开管道文件才会在内核空间创建管道。
mkfifo函数
函数说明
创建有名管道
函数原型
int mkfifo(const char *filename,mode\_t mode)
传入参数
filename:要创建的 FIFO 文件的全路径名。
mode:文件访问权限,最终的权限与umask相关。
返回值
成功返回0,出错返回-1。
有名管道的打开
调用open函数就可以打开管道文件。对于为了读而打开的管道文件在调用open函数时需要在参数中加上只读选项O\_RDONLY,对于为写而打开的管道在调用open函数时需要在参数中加上只写选项O\_WRONLY。值得注意的是,调用open函数时读写端同时存在才能打开管道文件,否则进程会阻塞。若在open函数的参数中加入O\_NONBLOCK 选项,进程就不会阻塞。
有名管道的关闭
调用close函数就可以关闭管道文件。
有名管道的读写
调用read、write函数就可以读写管道文件。当写端关闭时,读端的read会返回0表示写端关闭。当读端关闭时,写端写入数据会造成管道断裂,进程收到 SIGPIPE的信号。
信号
基本概念
信号是在软件层面上对中断机制的一种模拟,是一种异步通信方式。在命令行输入kill -l可以看到64种信号,其中前31种信号为不可靠信号,不支持信号排队,连续两个相邻的同样的信号会造成后一个信号丢失。用户对信号的响应有以下三种方式:
- 忽略信号:除SIGKILL和SIGSTOP信号外,用户可以忽略其它信号。
- 捕捉信号:定义信号处理函数,当信号来时会进入定义好的信号处理函数。
- 默认处理:根据默认的处理方式处理。比如:终止进程、暂停进程等。
信号发送
说明
进程的发送函数有kill、raise、alarm。
kill函数
函数说明
kill函数可以发送信号给进程或者进程组。
函数原型
int kill(pid\_t pid, int sig)
传入参数
pid:正整数:信号要发送到的进程的进程号;0:信号发送给与当前进程在同一个进程组的进程;-1:信号发送给所有进程表中的进程(除进程号最大的进程外);<-1:信号发送给进程组号为-pid的进程组里的所有进程。
sig:信号。
返回值
成功返回0,出错返回-1。
raise函数
函数说明
raise函数只能发送信号给自己。
函数原型
int raise(int sig)
传入参数
sig:信号。
返回值
成功返回0,出错返回-1。
alarm函数
函数说明
alarm函数被称为闹钟函数,当它设置的时间到时,会向进程发送SIGALARM 信号。一个进程只能由一个闹钟,如果在调用 alarm()之前已设置过闹钟,则以前的闹钟时间都被新值覆盖。
函数原型
unsigned int alarm(unsigned int seconds)
传入参数
seconds:seconds秒后发送SIGALRM信号给该进程。
返回值
如果调用此 alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟
时间的剩余时间,否则返回 0,出错返回-1。
信号接收
进程中在等待接收信号主要有这几种方式:死循环while(1)、睡眠sleep和pause函数。
pause函数
函数说明
pause()函数是用于将调用进程挂起直至接收到信号为止。
函数原型
int pause(void)
返回值
成功返回0,出错返回-1。
信号处理
进程收到信号后不想用默认的处理方式,这样就需要使用single函数对信号处理方式进行设置。
signal函数
函数说明
调用signal可以设置该进程对某个信号的处理方式,可以设置为忽略、默认、自定义的处理方式。
函数原型
void (*signal(int signum, void (*handler)(int)))(int)
传入参数
signum:指定信号代码。
handler:SIG\_IGN:忽略该信号;SIG\_DFL:采用系统默认方式处理信号;自定义的信号处理函数指针。
返回值
成功以前的信号处理配置,出错返回-1。
- -
System V IPC
概述
System V IPC对象有共享内存、消息队列、信号灯。与管道通信不同的是,IPC对象在创建后会一直存在,直到被显式删除。每个IPC对象有唯一的ID,但是这个ID是随机创建的,其它进程无法获得这个ID,于是可以让每个IPC对象关联的一个Key,从而其它进程可以通过这个Key来访问IPC对象。使用ipcs命令可以显示所有的IPC对象,使用ipcrm命令可以删除一个IPC对象。
IPC对象的框架思路与文件IO的框架思路类比如下:
功能文件IOIPC打开、创建openshmget、msgget、semget读写read、writeshmat、shmdt、msgsnd、msgrecv、semop关闭closeshmctrl、msgctrl、semctrl
Key值
Key为IPC\_PRIVATE,即为0时表示IPC对象是私有对象,只有当前进程可以访问。如果IPC想被多个进程访问,就需要调用ftok函数生成一个非0的Key。
ftok函数
函数说明
生成一个非0的Key值。
函数原型
key\_t ftok(const char *path,int proj\_id)
传入参数
path:存在且可访问的文件的路径。
proj\_id:用户生成Key的数字,不能为0,一般会传入一个字符。
返回值
成功返回Key值,出错返回-1。
共享内存
概述
通过进程控制的学习我们知道了进程的虚拟内存中有一个映射段,共享内存就是将内核空间为共享内存留出的内存区映射到了用户空间的映射段,进程可以直接读写映射段的内存,不需要任何数据的复制,读完后数据依旧保留在共享内存中,使用十分灵活。因此共享内存是一种最为高效的进程间通信方式。同时需要注意的是,多个进程共享一段内存需要依靠同步和互斥机制来确保进程不会同时操作共享内存。
共享内存的使用步骤
- 创建/打开共享内存
- 映射共享内存
- 读写共享内存
- 撤销共享内存映射
- 删除共享内存对象。
创建/打开共享内存
shmget函数
函数说明
创建/打开共享内存。
函数原型
int shmget(key\_t key, int size, int shmflg)
传入参数
key:共享内存的Key值,可以使用ftok生成的Key值或者IPC\_PRIVATE。
size:共享内存区大小。
shmflg:文件访问权限,最终的权限与umask相关。
返回值
成功返回共享内存段标识符,出错返回-1。
映射共享内存
shmat函数
函数说明
映射共享内存。
函数原型
char *shmat(int shmid, const void *shmaddr, int shmflg)
传入参数
shmid:要映射的共享内存区标识符。
shmaddr:将共享内存映射到指定地址,若为 0 则表示系统自动分配地址并把该
段共享内存映射到调用进程的地址空间。
shmflg:SHM\_RDONLY:共享内存只读;0:共享内存可读可写。
返回值
成功返回被映射的段地址,出错返回-1。
撤销共享内存映射
shmdt函数
函数说明
撤销映射共享内存。
函数原型
int shmdt(const void *shmaddr)
传入参数
shmaddr:被映射的共享内存段地址
返回值
成功返回0,出错返回-1。
设置共享内存对象
shmctl函数
函数说明
设置共享内存。当调用shmctl函数删除共享内存时,共享内存并不会立刻被删除,而是被标记为删除,等待所有进程结束或撤销映射后才会被真正删除。
函数原型
int shmctl(int shmid,int cmd,struct shmid\_ds *buf)
传入参数
shmid:要映射的共享内存区标识符。
cmd:指定要执行的操作。IPC\_STAT:获取共享内存属性;IPC\_SET:设置共享内存属性;IPC\_RMID:删除共享内存。
buf:保存或设置共享内存属性的地址。
返回值
成功返回0,出错返回-1。
消息队列
概述
消息队列是一个消息的列表,由消息队列ID来唯一标识。。消息队列是一个链式队列,用户可以在消息队列中可以根据类型在链式队列的任意位置添加消息,读取消息。
消息队列的使用步骤
- 打开/创建消息队列
- 向消息队列发送消息
- 从消息队列接收消息
- 控制消息队列
打开/创建消息队列
msgget函数
函数说明
创建/打开消息队列。
函数原型
int msgget(key\_t key, int msgflg)
传入参数
key:共享内存的Key值,可以使用ftok生成的Key值或者IPC\_PRIVATE。
msgflg:文件访问权限,最终的权限与umask相关。
返回值
成功返回消息队列 ID,出错返回-1。
向消息队列发送消息
msgsnd函数
函数说明
向消息队列发送消息。
函数原型
int msgsnd(int msqid, const void *msgp, size\_t msgsz, int msgflg)
传入参数
msqid:消息队列的队列 ID。
msgp:指向消息结构的指针。
msgsz:消息正文的字节数,不包括消息类型指针变量。该消息结构 msgbuf 通常为:
struct msgbuf
{
long mtype; //消息类型
char mtext[N]; //消息正文
};
msgflg::IPC\_NOWAIT:若消息无法立即发送,如当前消息队列已满,函
数会立即返回;0:阻塞直到发送成功为止
返回值
成功返回0,出错返回-1。
从消息队列接收消息
msgrcv函数
函数说明
从消息队列接收消息。需要注意的是,从消息队列中读取消息后,队列中的消息就删除了。
函数原型
int msgrcv(int msgid, void *msgp, size\_t msgsz, long int msgtyp, int msgflg)
传入参数
msqid:消息队列的队列 ID。
msgp:消息缓冲区, 同于 msgsnd函数的 msgp。
msgsz:消息正文的字节数,不包括消息类型指针变量。
msgtyp:0:接收消息队列中第一个消息;大于 0:接收消息队列中第一个类型为 msgtyp的消息;小于 0:接收消息队列中第一个类型值不小于 msgtyp 绝对值且类型值又最小的消息。
msgflg:MSG\_NOERROR:若返回的消息比 msgsz 字节多,则消息就会截短到 msgsz 字节,且不通知消息发送进程;IPC\_NOWAIT: 若在消息队列中并没有相应类型的消息可以接收,则函数立即返回ENPMSG;0:若无消息会一直阻塞。
返回值
成功返回0,出错返回-1。
控制消息队列
msgctl函数
函数说明
控制消息队列。
函数原型
int msgctl (int msgqid, int cmd, struct msqid\_ds *buf )
传入参数
msqid:消息队列的队列 ID。
cmd:指定要执行的操作。IPC\_STAT:获取消息队列属性;IPC\_SET:设置消息队列属性;IPC\_RMID:删除消息队列。
buf:保存或设置消息队列属性的地址。
返回值
成功返回0,出错返回-1。
信号灯
概述
在操作系统多任务并发的情况下,多个进程会同时运行,为了实现某个目的需要不同进程协作完成,这就是同步。在不同的进程之间也可能存在争夺同一资源的情况,这就是互斥。
信号灯是用户进程/线程同步或互斥的机制。信号灯是一个或多个计数信号灯的集合,可同时操作集合中的多个信号灯。信号灯的机制是通过PV两个原子操作实现进程间的同步和互斥。信号灯是某一种资源,取非负的整数,信号灯的值表示可用资源的数量。P操作是指如果有可用资源,即信号灯值大于0,则占用一个资源,此时信号灯的值减1,如果信号灯值等于0,则阻塞等待。V操作是指释放一个资源,此时信号灯的值加1。
信号灯的使用步骤
- 创建信号灯
- 初始化信号灯
- P/V操作
- 删除信号灯
创建信号灯
semget函数
函数说明
创建信号灯。
函数原型
int semget(key\_t key, int nsems, int semflg)
传入参数
key:共享内存的Key值,可以使用ftok生成的Key值或者IPC\_PRIVATE。
nsems:需要创建的信号灯数目。
semflg:信号灯访问权限,最终的权限与umask相关。
返回值
成功返回信号灯标识符,出错返回-1。
设置信号灯
semctl函数
函数说明
设置信号灯。
函数原型
int semctl(int semid, int semnum, int cmd, union semun arg)
传入参数
semid:semget()函数返回的信号灯标识符。
semnum:信号灯编号。
cmd:指定要执行的操作。GETVAL:获取信号灯的值;SETVAL:设置信号灯的值;IPC\_RMID:删除信号灯。
返回值
成功返回0,出错返回-1。
P/V操作
semop函数
函数说明
P/V操作。
函数原型
int semop(int semid, struct sembuf *sops, size\_t nsops)
传入参数
semid:semget()函数返回的信号灯标识符。
sops:指向信号灯操作数组,一个数组包括以下成员:
struct sembuf
{
short sem_num;//信号量编号
short sem_op;// 信号量操作:取值为-1 则表示 P 操作,取值为+1 则表示 V 操作
short sem_flg;//0,IPC_NOWAIT,SEM_UNDO
};
nsops:要操作的信号灯的个数。
返回值
成功返回0,出错返回-1。
- -