一、介绍
进程间通信(IPC,InterProcess Communication),指的是在不同进程之间传播或交换信息。IPC 的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket 和 Streams 支持不同主机上的两个进程 IPC,以 Linux 中的 C 语言为例。
二、进程间通信的方式
2.1、管道
2.1.1.无名管道
无名管道是 UNIX 系统 IPC 最古老的形式,它的特点有以下:
- 半双工,数据只能在一个方向上流动,具有固定的读端和写端
- 只能用于具有情缘关系的进程(父子进程或兄弟进程)
- 管道并不保存数据,在管道写入数据,然后被读走,管道里就没有数据了
- 可以将其看成是一种特殊的文件,对于管道的读写也可以使用普通的 read、write 等函数,管道不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中
进程间使用管道进行通信,需要创建管道,该函数原型如下:
#include <unistd.h>
int pipe(int fd[2]); //返回值:0为成功,-1为失败
这个 fd 文件标识符是一个整型数组,fd[0] 是读端,fd[1] 是写端,若是读端的进程,需要关闭写段,然后调用 read 函数,写端也是同样的道理:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd[2];
pid_t pid;
char readBuf[128] = {0};
if(pipe(fd) == -1) //创建管道
{
printf("Creat pipe error!!!\n");
return 0;
}
else
{
pid = fork();
if(pid > 0) //父进程为写端
{
sleep(2);
close(fd[0]);
printf("This is the father's process:%d\n", getpid());
write(fd[1], "I'm your father", strlen("I'm your father"));
wait(NULL);
}
else if(pid == 0) //子进程为读端
{
close(fd[1]);
printf("This is the son's process:%d\n", getpid());
read(fd[0], readBuf, 128);
printf("Son read father's message:%s\n", readBuf);
exit(0);
}
else if(pid < 0)
{
printf("Creat process error!!!\n");
}
}
return 0;
}
2.1.2.命名管道,fifo
为了创建多个命名管道,让多个不相关的进程进行通信,因此它有一个属于自己的名字。使用 mkfifio 函数创建一个命名管道,该函数创建的是一个特殊的文件,该文件不存数据,只当一个管道。该函数的原型如下:
int mkfifo(const char *pathname, mode_t mode);
- pathname:在某个路径下,创建管道并命名
- mode:可读可写等等权限
下面是创建并读取管道 read.c 文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main()
{
int fd, n_read;
char readBuf[30] = {0};
if((mkfifo("./file", 0600) == -1) && errno != EEXIST) //创建名叫file的管道,0600:只有文件主人自己能读能写
{
printf("mkfifo failuer\n");
perror("why");
}
fd = open("./file", O_RDONLY); //以只读方式打开该管道
while(1)
{
n_read = read(fd, readBuf, 30); //读取该管道的数据
printf("The pipe have %d byte,context:%s\n", n_read, readBuf);
}
close(fd);
return 0;
}
下面是向该管道写入数据 write.c 文件,若先运行 read.c 文件,读取不到数据,则会阻塞在读取函数里不会运行以下程序:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd;
char *str = "Hello Linux!!!";
fd = open("./file", O_WRONLY); //以只写方式打开该管道
while(1)
{
sleep(3);
write(fd, str, strlen(str)); //向该管道写入数据
}
close(fd);
return 0;
}
接着打开两个终端,运行读取管道,该进程读不到数据并进行阻塞,直到另一个终端运行写入数据,两个进程进行正常通信。
2.2.消息队列
消息队列是进程间通信方式的一种,在 linux 内核中创建一条或多条消息队列,进程每次需要操作队列都需要访问到内核,自然速度就比较慢,实际上消息队列是由链表所造成,例如:进程 A 可以往指定的队列写入数据,接着进程 B 可以读取指定队列的数据。这两进程不需要同时存在,进程 A 可以往队列里发完消息就结束,进程 B 可以在之后任何时间再来取,它们之间可以进行异步通信。当创建多条队列时,进程们通过 msgget 函数所返回的消息队列的标识符进行识别,下面是 msgget 函数的介绍:
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
- key:该键值可以通过 ftok 函数生成,通信的两个进程需要给予该函数相同的键值才能打开该队列并且进行操作
- msgflg:标志位,可以通过按位或组合使用,IPC_CREAT 若不存在,就创建队列;IPC_EXCL 与 IPC_CREAT 一同使用,若队列已存在则失败;还有权限位,例如:0666,指定队列的读写权限
当队列创建完毕之后,就要调用 msgsnd 函数和 msgrcv 函数对该队列进行操作。下面是这两个函数的原型:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//成功返回0,失败返回-1
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
//成功返回实际接收到的消息数据字节数,失败返回-1
- msgid:消息队列 ID(由 msgget 返回)
- msgp:struct msgbuf 结构体(下文),里面内容有长整型数和字符型指针,发送方的 msgp 需要在结构体里写入数据;接收方的 msgp 则不需要
- msgsz:发送方,消息数据的长度;接收方,缓冲区大小
- msgtyp:指定要接收的消息类型,为 0,接收队列中的第一条消息(不管类型);> 0 接收类型等于发送方结构体里的 mtype;< 0 接收类型值 <= |msgtyp| 的最小类型的第一条消息
- msgflg:控制标志位,为 0 是阻塞模式,发送方,如果队列已满,进程会阻塞等待;接收方:如果没有消息,进程阻塞等待;IPC_NOWAIT 是非阻塞模式,发送方,如果队列已满,直接返回失败;接收方,如果没有消息,立即返回;接收方的 MSG_NOERROR,如果消息数据超过 msgsz,截断而不报错
想要发送或接收,需要通过以下结构体:
struct msgbuf {
long mtype; // 消息类型(必须 > 0)
char mtext[1024]; // 消息数据(可以是任意类型)
};
发送方可以指定每一条消息都有类型,接收方可以选择接收什么类型的消息,如果消息匹配的话就接收。
下面是创建,接收另一个进程发来的消息并回应的程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf {
long mtype;
char mtext[128];
};
int main()
{
int msgid;
struct msgbuf readBuf;
struct msgbuf sendBuf = {124, "I'm find thank you."};
msgid = msgget(0x1234, IPC_CREAT|0777);
if(msgid == -1)
{
printf("Creat fail!!!\n");
}
msgrcv(msgid, &readBuf, sizeof(readBuf.mtext), 123, 0);
printf("receive from 123send:%s\n", readBuf.mtext);
msgsnd(msgid, &sendBuf, strlen(sendBuf.mtext), 0);
return 0;
}
下面是发送之后等待回复的程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
int msgid;
struct msgbuf sendBuf = {123, "How are you?"};
struct msgbuf readBuf;
msgid = msgget(0x1234, IPC_CREAT|0777); //相同的键值才能获取相同的队列ID
if(msgid == -1)
{
printf("Creat fail\n");
}
msgsnd(msgid, &sendBuf, strlen(sendBuf.mtext), 0);
printf("Send over\n");
msgrcv(msgid, &readBuf, sizeof(readBuf.mtext), 124, 0);
printf("receive from 124send:%s\n", readBuf.mtext);
return 0;
}
2.3.共享内存
共享内存是最快的进程间通信方式,适合传输大量数据,它允许多个进程直接读写同一块物理内存,而无需在进程间复制数据。它的核心是在物理内存中开辟一块区域,多个进程可以将该区域映射到自己的虚拟地址空间,进程可以直接通过指针访问这块内存,就像访问普通内存一样,写入可以使用 strcpy,读出可以直接使用 printf 即可。共享内存的步骤如下:
- shmget,先是得到该共享内存的 ID
- shmat,接着将共享内存段附加(attach)到当前进程的地址空间,使进程能够访问该共享内存。其返回值是一个指向这块区域的指针
- 对这块区域进行读写操作
- shmdt,分离共享内存,每个进程附加共享内存都会增加系统的引用计数。如果不分离共享内存会导致下面几点,即使进程退出,共享内存可能仍然被标记为"在使用";内存泄露,导致电脑内存不够使用;其他的进程意外修改此内存
- shmctl(IPC_RMID),删除这一块的共享内存,如果不删除,即使所有进程都退出,共享内存仍然占用系统资源;系统资源回收,可通过
ipcs -m指令查看未清理的共享内存
以下是函数的原型:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg); //size,以1字节对齐
void *shmat(int shmid, const void *shmaddr, int shmflg); //第二个参数一般写NULL,由系统自动分配合适的地址,第三个参数,0为只写(默认),SHM_RDONLY为只读
int shmdt(const void *shmaddr); //shmat返回的地址指针
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
下面使用共享内存的进程间通信,写入数据的程序:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main()
{
key_t key;
int shmid;
char *shmaddr;
key = ftok(".", 1); //获取键值
shmid = shmget(key, 1024*2, IPC_CREAT|0666); //创建共享内存,并获取其ID
if(shmid == -1)
{
printf("Creat shm error!!!\n");
exit(-1);
}
shmaddr = shmat(shmid, NULL, 0); //映射到该进程的内存上,第二个参数填入NULL表示让系统自动分配地址,并返回得到共享内存的指针
strcpy(shmaddr, "Hello linux"); //将该字符串写入到该地址上
sleep(3);
shmdt(shmaddr); //分离该地址,防止内存泄漏、意外修改内存数据
shmctl(shmid, IPC_RMID, 0); //删除共享内存,让系统回收资源
printf("Quit\n");
return 0;
}
下面是读取数据的程序:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
int main()
{
key_t key;
int shmid;
char *shmaddr;
key = ftok(".", 1);
shmid = shmget(key, 1024*2, 0); //已知共享内存被创建,只需获取其共享内存的ID即可
if(shmid == -1)
{
printf("Creat shm error!!!\n");
exit(-1);
}
shmaddr = shmat(shmid, 0, 0);
printf("Data:%s\n", shmaddr);
shmdt(shmaddr);
printf("Quit\n");
return 0;
}
2.4.信号
信号,又称为软中断信号,是 linux 系统响应某些条件而产生的一个事件。它是操作系统向一个进程或者线程发送的一种异步通知,用于通知该进程或线程某种事件已经发生,需要做出相应的处理。例如:终端用户输入了 ctrl + c 来中断程序,会通过信号机制来停止一个程序。在终端中,可以输入kill -l来查看具体的信号名称和序号:

处理信号的方式有以下三种:
- 捕捉信号:写一个信号处理函数告诉内核,用户需要这样去处理该信号,该函数由用户自定义
- 无视信号:使用
SIG_IGN这个宏来说明该信号需被无视,大多数信号可以使用该方法,但SIGKILL和SIGSTOP无法忽视,因为它们是可靠的进程终止和停止的方法,如果连这个都能被取代,那么没人能管得住这个无法停止的进程 - 系统默认动作:对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行,例如:ctrl + c 停止该进程
信号的捕捉可以使用 signal 函数,它的函数原型:
#include <signal.h>
typedef void (*sighandler_t)(int); //定义一个void返回类型的函数,它的参数是signum(信号的序号)
sighandler_t signal(int signum, sighandler_t handler);
当检测到信号与 signum 相同时,就会进入信号处理函数 handler 做出相应工作:
#include <stdio.h>
#include <signal.h>
void handler(int signum)
{
printf("you can't kill me\n");
}
int main()
{
signal(2, handler); //当收到ctrl+c终止信号,但是程序改变了该信号的默认功能
// signal(2, SIG_IGN); //当收到ctrl+c终止信号,进行无视
while(1);
return 0;
}
停止进程的 ctrl + c 被修改了,怎么办?还有一个方法,在另一个终端输入kill -SIGKILL 进程ID即可终止该进程。当然能让另一个程序发出信号,可以使用 kill 函数,下面是该函数的原型:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig); //参数1:接收该信号的pid;参数2:信号
编写一个程序,该程序有两个参数,第一个参数是信号编号,第二个参数是接收方的 pid:
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc, char **argv)
{
int signum;
pid_t pid;
char arr[128] = {0};
signum = atoi(argv[1]);
pid = atoi(argv[2]);
//sprintf的用法,第一个参数是承载字符串的指针,后面的参数就与printf一致,装好的arr可以提供下面的system函数使用
// sprintf(arr, "kill -%d %d", signum, pid);
// system(arr);
kill(pid, signum);
return 0;
}
只是简单信号传输好像还不够完整,如果信号的到来还加上数据的传递就好了,因此衍生出以下两个函数:
- 知道由哪一个进程发来的信号并读取该信号所带来的消息:sigaction
#include <signal.h>
int sigaction(int signum, //应该接收到信号的序号
const struct sigaction *act, //结构体,里面存的由有谁发送的信号、消息
struct sigaction *oldact); //结构体,备份原有的信号操作,一般填写NULL
其中struct sigaction的成员有:
struct sigaction {
void (*sa_handler)(int); //函数指针,如果配置这个,那整个sigaction函数与signal函数一样,因此有数据传输的信号不会配置该项
void (*sa_sigaction)(int, siginfo_t *, void *);//函数指针,有数据传输就需要配置该项
sigset_t sa_mask; //阻塞
int sa_flags; //标志位,配置成SA_SIGINFO意味着有数据传递
};
- 对哪一个进程,通过什么信号,发送什么数据的函数:sigqueue
#include <signal.h>
//接收信号的进程ID;信号的序号;需要传输的数据
int sigqueue(pid_t pid, int sig, const union sigval value);
第三个参数是一个联合体,下面是它的成员:
union sigval {
int sival_int; //发送int型数据
void *sival_ptr; //发送指针
};
通过例子来说明这两个函数的使用方法,下面是接收方的程序:
#include <stdio.h>
#include <signal.h>
//void类型 函数名字(参数)
//void (*sa_sigaction)(int, siginfo_t *, void *);
void handler(int signum, siginfo_t *info, void *context)
{
printf("The number of signal is:%d\n", signum);
if(context != NULL)
{
printf("1data:%d\n", info->si_int); //读取该信号带来的消息
printf("2data:%d\n", info->si_value.sival_int); //与上面一样
printf("Receive from:%d\n", info->si_pid); //让用户知道是哪一个进程发来的信号
}
}
int main()
{
struct sigaction act;
printf("Please input %d to send message\n", getpid());
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGUSR1, &act, NULL); //如果收到SIGUSR1的信号,就读取该信号所带来的数据
while(1);
return 0;
}
下面是发送方:
#include <stdio.h>
#include <signal.h>
//需要输入两个参数,第一个输入信号的序号,第二个输入接收方的进程ID
int main(int argc, char **argv)
{
int signum;
pid_t pid;
//将字符的数字转变成计算机可以计算的整型数
signum = atoi(argv[1]);
pid = atoi(argv[2]);
union sigval value;
value.sival_int = 250; //需要发的数据
sigqueue(pid, signum, value);
printf("send signal'pid:%d\n", getpid());
return 0;
}
2.5.信号量
信号量用于控制对共享资源(一块内存、一个文件、一个硬件设备)的访问,例如:当进程 A 获取了仅有的信号量并正在向一块内存写入数据,进程 B 也需要向内存写入数据但没有获取信号量,自然需要等待进程 A 释放信号量,进程 B 得到信号量之后才能进行写入数据。这样看来信号量保护了这一块内存,避免了两个进程同时写入的混乱。在 Linux 中,主要有两种信号量:
- 内核信号量:由内核管理,主要用于进程之间的同步;即使某个进程在持有信号量时被强制杀死,内核通常也能确保信号量被正确释放,避免资源被永远锁住
- POSIX 信号量:遵循 POSIX 标准,可以用于进程间或线程间的同步;无名信号量:通常用于同一个进程内的多个线程之间的同步,存储在内存中(如全局变量);有名信号量:通过一个名字来标识,存储在文件中(如 /dev/shm 下),可以被多个不相关的进程访问,用于进程间同步
在 System V IPC 中,信号量通常以"集合"的形式存在。用户可以创建一个包含多个信号量的集合,然后同时操作它们。这三个函数就是用来管理这种信号量集的:
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */); //必须是一个联合体结尾
int semop(int semid, struct sembuf *sops, size_t nsops);
直接上例子说明:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
void pGetKey(int semID) //获取信号量
{
//这个结构体是一个结构体数组,但例子的信号集只有一个信号量
struct sembuf sops;
sops.sem_num = 0; //信号量的序号,0为第1个
sops.sem_op = -1; //信号量数值-1
sops.sem_flg = SEM_UNDO; //得不到信号量就一直等待
semop(semID, &sops, 1);
}
void vPutBack(int semID) //归还信号量
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
semop(semID, &sops, 1);
}
int main()
{
key_t key;
pid_t pid;
int semID;
key = ftok(".", 1);
//参数:1.获取键值;2.需要创建的信号量的数量;3.信号量权限,与文件权限一样
semID = semget(key, 1, IPC_CREAT|0666); //获取/创建信号量
union semun initsem;
initsem.val = 0; //为了先让子进程先运行,信号量初始为0,并让子进程运行之后再释放信号量
//控制信号量的参数:1.信号量ID;2.需要操作信号量的序号,0代表第1个;3.联合体,配置信号量的初始值的数量
semctl(semID, 0, SETVAL, initsem);
pid = fork(); //创建父子进程
if(pid > 0) //父进程
{
pGetKey(semID);
printf("This is the father process\n");
vPutBack(semID);
}
else if(pid == 0) //子进程
{
printf("This is the son process\n");
vPutBack(semID);
}
else
{
printf("Error\n");
}
return 0;
}
1877

被折叠的 条评论
为什么被折叠?



