进程间通信
文章目录
1 概述
进程间通信(InterProcess Communication, IPC)方式主要有以下几种:
管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道具有管道所具有的功能且允许无亲缘关系进程间的通信。
信号(Signal):信号是在软件层次上对中断机制的一种模拟,用于通知进程有某事件发生。
消息队列(Messge Queue):消息队列是消息的链接表,克服了前两种通信方式中信息量有限的缺点。
共享内存(Shared memory):可以说是最有用的进程间通信方式,使多个进程可以访问同一块内存空间,不同进程可以看到对方进程中对共享内存中数据的更新。
信号量(Semaphore):主要作为进程间以及同一进程的不同线程之间的同步和互斥手段。
套接字(Socket):可用于网络中不同机器之间的进程间通 信,应用非常广泛。
2 管道
2.1 概述
无名管道具有如下特点。
(1)只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间)。
(2)是一种半双工的通信模式,具有固定的读端和写端。
(3)可以看成是一种特殊的文件,对于它的读写也可以使用普通的read和write等函数。但它不属于其他任何文件系统,只存在于内核的内存空间中。
2.2 管道系统调用
管道创建与关闭:管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fds[0]和fds[1],其中fds[0]固定用于读管道,而 fd[1]固定用于写管道,构成了一个半双工的通道。创建管道可以通过调用 pipe来实现。
#include <unistd.h>
int pipe(int fd[2]);
//成功返回0,出错返回-1
fd[2]:管道的两个文件描述符 。
管道关闭可使用close函数逐个关闭各个文件描述符。
管道读写:通常先创建一个管道,再通过 fork()函数创建一子进程,该子进程会继承父进程所创建的管道,父子进程分别拥有自己的读写通道,为了实现父子进程之间的读写,只需把无关的读端或写端的文件描述符关闭即可。
父进程写入子进程读取:父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1])。
子进程写入父进程读取:父进程关闭fd[1],子进程关闭fd[0]。
注意:
(1)当写一个读端被关闭的管道时,内核将产生SIGPIPE。
(2)向管道写入数据时,Linux 将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。
2.3 标准流管道
说明:管道的操作支持基于文件流的模式,主要是用来创建一个连接到另一个进程的管道。这里的“另一个进程”就是一个可以进 行一定操作的可执行文件。标准流管道的创建过程由函数popen完成,它完成以下工作:创建一个管道,fork一个子进程,在父子进程中关闭不需要的文件描述符,执行 exec 函数族调用,执行函数中所指定的命令。关闭用 popen创建的流管道必须使用函数pclose。
#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
//成功返回文件指针,出错返回NULL
int pclose(FILE *fp);
//成功返回cmdstring的终止状态,出错返回-1
type:若为“r”,则文件指针连接到cmdstring的标准输出。若为“w”,则文件指针连接到cmdstring的标准输入。
3 FIFO
3.1 概述
FIFO也叫命名管道,它可以使互不相关的两个进程实现彼此通信。FIFO是一种文件类型,创建FIFO类似于创建文件,可以把它当作普通文件一样进行读写操作,FIFO严格地遵循先进先出规则,对管道及 FIFO 的读总是从开始 处返回数据,对它们的写则把数据添加到末尾,不支持如 lseek等文件定位操作。
普通文件的读写时不会出现阻塞问题,而在管道的读写中却有 阻塞的可能,这里的非阻塞标志可以在 open()函数中设定为 O_NONBLOCK。
(1)对于读进程:若该管道是阻塞打开,且当前 FIFO 内没有数据,则对读进程而言将一直阻塞到有数据写入。若该管道是非阻塞打开,则不论FIFO内是否有数据,读进程都会立即执行读操作。即如果 FIFO 内没有数据,则读函数将立刻返回0。
(2)对于写进程:若该管道是阻塞打开,则写操作将一直阻塞到数据可以被写入。若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败。
3.2 mkfifo函数
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
//成功返回0,出错返回-1
mode:与open函数中的mode相同。
4 信号
4.1 概述
信号是一种软件中断,是一 种异步通信方式。它可以在任何时候发给某一进程,而无需知道该进程的状态。
一个完整的信号生命周期可以分为 3 个重要阶段:信号产生、信号在进程中注册和注销、执行信号处理函数。
进程对信号的处理:
(1)忽略此信号,,即对信号不做任何处理。但有两种信号不能忽略:SIGKILL和SIGSTOP,因为它们向超级用户提供了使进程终止或停止的方法。
(2)捕捉信号。通知内核在某种信号发生时调用一个用户函数,在用户函数中执行对这种事件的处理。
(3)执行系统默认动作。大多数默认动作是终止该进程,并在进程当前工作目录的core文件中复制了该进程的内存映像,调试程序使用core文件检查进程终止时的状态。
常见信号含义及默认操作
信号名 | 含义 | 默认操作 |
---|---|---|
SIGHUP | 用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一会话内的各个作业与控制终端不再关联 | 终止 |
SIGINT | 用户键入INTR字符(通常是 Ctrl+C)时发出,终端驱动程序发送此信号并送到前台进程中的每一个进程 | |
SIGQUIT | 该信号和 SIGINT 类似,但由QUIT字符(通常是 Ctrl+\)来控制 | |
SIGILL | 一个进程企图执行一条非法指令时发出 | |
SIGFPE | 该信号在发生致命的算术运算错误时发出。 | |
SIGKILL | 立即结束程序的运行 | |
SIGALRM | 该信号当一个定时器到时的时候发出 | |
SIGSTOP | 该信号用于暂停一个进程,且不能被阻塞、处理或忽略 | 暂停进程 |
SIGTSTP | 该信号用于交互停止进程,用户键入SUSP 字符时(通常是Ctrl+Z)发出这个信号 | 停止进程 |
SIGCHLD | 子进程改变状态时,父进程会收到这个信号 | |
SIGABORT | 进程异常终止时发出 |
4.2 信号发送与捕捉
kill函数可以发送信号给进程或进程组。raise函数则允许进程向自身发送信号。
#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid, int signo);
int raise(int signo);
//成功返回0,出错返回-1
pid:
pid>0:将信号发给进程ID为pid的进程。
pid=0:将信号发给与发送进程属于同一进程组的所有进程。
pid<0:将信号发给进程ID等于pid绝对值。
pid=-1:将信号发给进程有权向它们发送信号的所有进程。
signo:信号。
alarm函数可以设置一个定时器,当定时器超时时,产生SIGALRM信号。若忽略或不捕捉此信号,则默认动作时终止调用该alarm函数的进程。pause函数使调用进程挂起直至捕捉到一个信号。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//成功返回0或以前设置的闹钟时间秒数
int pause(void);
//只有执行了一个信号处理程序并从其返回时,pause返回-1,errno设置为EINTR
seconds是产生信号SIGALRM需要经过的时钟秒数。每个进程只能有一个闹钟时间。
4.3 信号的处理
signal函数信号机制最简单的接口,只需要指出要处理的信号和处理函数即可。
#include <signal.h>
//以下常量用于表示“指向函数的指针,该函数有一个整型参数,无返回值”
#define SIG_ERR (void (*)())-1
#define SIG_DFL (void (*)())0
#define SIG_IGN (void (*)())1
void (*signal(int signo, void (*func)(int)))(int);
//成功返回以前的信号处理配置,出错返回SIG_ERR
//signal函数原型有些复杂,可用typedef简化
typedef void Sigfunc(int);
Sigfunc *signal(int signo, Sigfunc *func);
signo是信号。
func是函数指针,指向的函数有一个整型参数,无返回值。可以是常量SIG_IGN(表示忽略此信号)、常量SIG_DFL(系统默认动作处理),或者是要调用的函数的地址,称这种处理为捕捉该信号,称此函数为信号处理函数(signal handler)。
signal函数的返回值是一个函数地址,该函数有一个整型参数(即最后的(int))。也就是说,signal的返回值是指向在此之前的信号处理程序的指针。
sigaction函数检查或修改与指定信号相关联的处理动作。
#include <signal.h>
struct sigaction
{
void (*sa_handler)(int); //信号捕捉函数的地址
sigset_t sa_mask; //信号集,可指定在信号处理程序执行过程中哪些信号应当被屏蔽
int sa_flags; //指定对信号进行处理的各个选项
void (*sa_sigaction)(int, siginfo_t *, void *); //替代的信号处理程序
};
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
//成功返回0,出错返回-1
/* 用sigaction实现signal函数 */
typedef void Sigfunc(int);
Sigfunc *signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}
signo是信号编号。若act非空则修改其动作。若oact非空则经由oact返回该信号的上一个动作。
4.4 信号集函数组
信号集函数组的功能包括:创建信号集合、注册信号处理函数以及检测信号。
sigemptyset将信号集合初始化为空。sigfillset将信号集合初始化为包含所有已定义的信号的集合。sigaddset将指定信号加入到信号集合中去。sigdelset将指定信号从信号集合中删除。sigismember查询指定信号是否在信号集合之中。
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
int sigpending(sigset_t *set);
//成功返回0,出错返回-1
int sigismember(sigset_t *set, int signum);
//成功返回1,出错返回0
set:信号集。
signum:指定信号代码。
how:决定函数的操作方式。
处理信号的操作流程:
定义信号集合、设置信号屏蔽位、定义信号处理函数、测试信号。
5 信号量
5.1 概述
同步关系:多个进程 可能为了完成同一个任务会相互协作。
互斥关系:不同进程之间,为了争夺有 限的系统资源(硬件或软件资源)会进入竞争状态。
临界资源:是在同一个时刻只允许有限个(通常只有 一个)进程可以访问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器以及其他外 围设备等)和软件资源(共享代码段,共享结构和变量等)。
临界区:访问临界资源的代码。
信号量:用来解决进程之间的同步与互斥问题的一种进程之间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(PV 操作)。信号量值指的是当前可用的该资源的数量,若它等于 0 则意味着目前没有可 用的资源。
P 操作:如果有可用的资源(信号量值>0),则占用一个资源(给信号量值减去一,进入临界区代码);如 果没有可用的资源(信号量值等于 0),则被阻塞到,直到系统将资源分配给该进程(进入等待队列,一直 等到资源轮到该进程)。
V 操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程。如果没有进程等待它, 则释放一个资源(给信号量值加一)。
5.2 信号量的应用
使用信号量的步骤:
(1)创建信号量或获得在系统已存在的信号量,此时需要调用 semget函数。
(2)初始化信号量,此时使用 semctl函数的 SETVAL 操作。
(3)进行信号量的 PV 操作,此时调用 semop函数。
(4)若不需要信号量,则从系统中删除它,此时使用 semclt函数的 IPC_RMID 操作。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
//成功返回信号量标识符(semid),出错返回-1
//semun结构须由程序员自己定义
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
int semctl(int semid, int semnum, int cmd, union semun arg);
//成功根据cmd值的不同返回不同值,出错返回-1
struct sembuf
{
short sem_num; /* 信号量编号,使用单个信号量时,通常取值为 0 */
short sem_op; /* 信号量操作:取值为-1 则表示 P 操作,取值为+1 则表示 V 操作*/
short sem_flg; /* 通常设置为SEM_UNDO,系统自动释放该进程中未释放的信号量 */
}
int semop(int semid, struct sembuf *sops, size_t nsops);
//成功返回信号量标识符,出错返回-1
key:信号量的键值,多个进程可以通过它访问同一个信号量,特殊值 IPC_PRIVATE用于创建当前进程的私有信号量 。
nsems:需要创建的信号量数目,通常取值为 1。
semflg:同open函数的权限位,若为IPC_CREAT标志创建新的信号量,如果同时使用 IPC_EXCL 标 志可以创建一个新的唯一的信号量,此时如果该信号量已经存在,该函数返回出错。
cmd:指定对信号量的各种操作。若为IPC_GETVAL则函数返回信号量当前值,否则返回0。
IPC_STAT:获得该信号量(或者信号量集合)的 semid_ds 结构,并存放在由第 4 个参数 arg 的 buf 指向的 semid_ds 结构中。
IPC_SETVAL:将信号量值设置为 arg 的 val 值。
IPC_GETVAL:返回信号量的当前值。
IPC_RMID:从系统中,删除信号量(或者信号量集) 。
sops:指向信号量操作数组的指针。
nsops:操作数组sops中的元素个数。
6 共享内存
6.1 概述
共享内存是一种为高效的进程间通信方式。因为进程可以直接读写内存,不需要任何数据的复制。为了在多个进程间交换信息,内核专门留出了一块内存区。这段内存区可以由需要访问的进 程将其映射到自己的私有地址空间。
6.2 共享内存的应用
实现:第一步是创建共享内存,使用函数shmget,也就是从内存中获得一段共享内存区域,第二步映射共享内存,也就是把这段创建的共享内存映射到具体的进程空间中,使用函数shmat。撤销映射使用函数shmdt。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int shmflg);
//成功返回共享内存标识符(shmid),出错返回-1
char *shmat(int shmid, const void *shmaddr, int shmflg);
//成功返回被映射的地址,出错返回-1
int shmdt(const void *shmaddr);
//成功返回0,出错返回-1
key:共享内存的键值,多个进程可以通过它访问同一个共享内存,特殊值 IPC_PRIVATE用于创建当前进程的私有共享内存 。
size:共享内存区大小 。
shmflg:同 open()函数的权限位。
shmaddr:将共享内存映射到指定地址(若为 0 则表示系统自动分配地址并把该 段共享内存映射到调用进程的地址空间) 。
shmflag:SHM_RDONLY即共享内存只读,默认0即共享内存可读写 。
7 消息队列
7.1 概述
消息队列就是一些消息的列表。可以实现消息的随机查询,消息存在于内核中的,由“队列 ID”来标识。
7.2 应用
消息队列的实现包括:
(1)创建或打开消息队列使用函数msgget,里创建的消息队列的数量会受到系统消息队列数量的限制。
(2)添加消息使用函数msgsnd,把消息添加到已打开的消息队列末尾。
(3)读取消息使用函数msgrcv,把消息从消息队列中取走,可指定取走某一条消息。
(4)控制消息队列使用函数msgctl,它可以完成多项功能。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int msgget(key_t key, int msgflg);
//成功返回消息队列ID(msqid),出错返回-1
struct msgbuf
{
long mtype; /* 消息类型,该结构必须从这个域开始 */
char mtext[1]; /* 消息正文 */
}
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
int msgrcv(int msgid, void *msgp, size_t msgsz, long int msgtyp, int msgflg);
int msgctl (int msgqid, int cmd, struct msqid_ds *buf );
//成功返回0,出错返回-1
key:消息队列的键值,多个进程可以通过它访问同一个消息队列,特殊值 IPC_PRIVATE创建当前进程的私有消息队列 。
msgflg:权限标志位。
msgp:指向消息结构的指针。
msgsz:消息正文的字节数(不包括消息类型指针变量)。
msgflg:为IPC_NOWAIT时若消息无法立即发送函 数会立即返回 ,为0时msgsnd调用阻塞直到发送成功为止 。
msgtyp:
等于0:接收消息队列中第一个消息 ;
大于0:接收消息队列中第一个类型为 msgtyp 的消息;
小于0:接收消息队列中第一个类型值不小于 msgtyp 绝对值 且类型值又小的消息 。
msgflg:
MSG_NOERROR:若返回的消息比 msgsz 字节多,则消息就 会截短到 msgsz 字节,且不通知消息发送进程。
IPC_NOWAIT:若在消息队列中并没有相应类型的消息可以接 收,则函数立即返回 。
0:msgsnd调用阻塞直到接收一条相应类型的消息为止 。
获取共享资源进程需执行以下操作:
(1)测试控制该资源的信号量。
(2)若此信号量为正,则进程可以使用该资源,此时,信号量减1,表示使用了一个资源单位。
(3)若此信号量为0,则进程进入休眠状态,直至信号量大于0。
当进程不再使用一个信号量控制的资源时,该信号量增1,若有进程在休眠等待此信号量则唤醒它们。
首先通过调用函数semget来获得一个信号量ID。
可重入函数:
进程捕捉到信号并对其进行处理时,正在执行的指令序列被信号处理程序临时中断,先执行信号处理程序中的指令,若从信号处理程序返回,则继续执行捕捉到信号时正在执行的指令序列。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处,故信号处理程序中应保证调用安全的函数(是可重入的并是异步信号安全的)。若在信号处理程序中调用不可重入函数,其结果是不可预测的。
不可重入信号:
(1)使用静态数据结构。
(2)调用malloc或free。
(3)是标准I/O函数。
获取共享资源进程需执行以下操作:
(1)测试控制该资源的信号量。
(2)若此信号量为正,则进程可以使用该资源,此时,信号量减1,表示使用了一个资源单位。
(3)若此信号量为0,则进程进入休眠状态,直至信号量大于0。
当进程不再使用一个信号量控制的资源时,该信号量增1,若有进程在休眠等待此信号量则唤醒它们。
首先通过调用函数semget来获得一个信号量ID。
可重入函数:
进程捕捉到信号并对其进行处理时,正在执行的指令序列被信号处理程序临时中断,先执行信号处理程序中的指令,若从信号处理程序返回,则继续执行捕捉到信号时正在执行的指令序列。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处,故信号处理程序中应保证调用安全的函数(是可重入的并是异步信号安全的)。若在信号处理程序中调用不可重入函数,其结果是不可预测的。
不可重入信号:
(1)使用静态数据结构。
(2)调用malloc或free。
(3)是标准I/O函数。
当在信号处理程序调用不可重入函数时,应当在调用前保存errno,在调用后恢复errno。