原文链接1: https://blog.csdn.net/qq_43519025/article/details/120798727
原文链接2: https://blog.csdn.net/qq_46323094/article/details/117441666
一、前言
- 进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。
二、进程间通信有哪几种方式
无名管道、FIFO也称有名管道、消息队列、共享内存、信号、信号量。
三、无名管道
-
特点:
1.1 是一种半双工的通信方式,因为一方想进行写入或者读取数据时,都需要先关闭对方的通道;
1.2 只能在具有亲缘关系的进程间使用,进程的亲缘关系一般指的是父子关系;
1.3 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。 -
函数原型:
#include <unistd.h> int pipe(int pipefd[2]);
-
源码
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { int fd[2]; char readbuf[128] = {0}; int pi_ret = pipe(fd); if(pi_ret < 0) { printf("creat IPC failed\n"); } else { if(fork() > 0) { printf("this is child process\n"); close(fd[1]); read(fd[0], readbuf, 128); printf("readbuf = %s\n", readbuf); exit(0); } else { printf("this is father process\n"); close(fd[0]); write(fd[1], "hello world", strlen("hello world")); wait(NULL); } } return 0; }
四、FIFO(有名管道)
-
特点
1.1 它是一种文件类型;但也是半双工通信,因为不管是读取还是写入时都会阻塞。
1.2 FIFO可以在无关的进程之间交换数据,与无名管道不同;
1.3 FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。 -
函数原型
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
2.1 其中的 mode 参数与 open 函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件 I/O 函数操作它。
2.2 当 open 一个 FIFO 时,是否设置非阻塞标志(O_NONBLOCK)的区别:若没有指定 O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。 若指定了 O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其 errno 置 ENXIO。 使用 mkfifo 创建FIFO文件,filename为文件的路径,mode为文件权限,在这里要注意掩码对于创建文件权限的影响。创建失败返回-1,如果FIFO文件已存在,errno值为EEXIST。
-
源码
write.c#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> int main(void) { int fd; if((mkfifo("./FIFO", 0600) == -1) && (errno != EEXIST)) { printf("creat fifo failed\n"); perror("why:"); } fd = open("./FIFO", O_WRONLY); printf("open write success\n"); while(1) { write(fd, "from IPC", strlen("from IPC")); sleep(1); } close(fd); return 0; }
read.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> int main(void) { int fd; char buf[30] = {0}; if((mkfifo("./FIFO", 0600) == -1) && (errno != EEXIST)) { printf("creat fifo failed\n"); perror("why:"); } fd = open("./FIFO", O_RDONLY); printf("open read success\n"); while(1) { read(fd, buf, 30); printf("readbuf = %s\n", buf); } return 0; }
五、消息队列
- 说明
1.1 消息队列,是消息的链接表,存放在内核之中。一个消息队列由一个标识符(即队列ID)来标识,每个消息队列中,又根据不同的消息Type来区分数据。
1.2 用户进程可以向消息队列添加消息,也可以向消息队列读取消息。 - 特点
2.1 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
2.2 消息队列是独立于发送和接收进程的,进程终止时,消息队列及其内容并不会被删除;
2.3 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
2.4 消息队列是IPC中的一种全双工通信方式。 - 消息队列函数的原型:
// 创建或打开消息队列:成功返回队列ID,失败返回-1 int msgget(key_t key, int flag); // 添加消息:成功返回0,失败返回-1 int msgsnd(int msqid, const void *ptr, size_t size, int flag); // 读取消息:成功返回消息数据的长度,失败返回-1 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag); // 控制消息队列:成功返回0,失败返回-1 int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 源码
write.c
read.c#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> //int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); //ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, // int msgflg); //int msgget(key_t key, int msgflg); //int msgctl(int msqid, int cmd, struct msqid_ds *buf); struct msgbuf { long mtype; /* message type, must be > 0 */ char mtext[128]; /* message data */ }; int main(void) { key_t key; struct msgbuf readbuf = {0}; struct msgbuf sendbuf = {888, "hello world" }; key = ftok(".", 1); int msgId = msgget(key, IPC_CREAT|0777); if(msgId == -1) { printf("magget error\n"); exit(-1); } msgsnd(msgId, &sendbuf, sizeof(sendbuf.mtext), 0); msgrcv(msgId, &readbuf, sizeof(readbuf.mtext), 889, 0); printf("readbuf2 = %s\n", readbuf.mtext); msgctl(msgId, IPC_RMID, NULL); return 0; }
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> //int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); //ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, // int msgflg); //int msgget(key_t key, int msgflg); //int msgctl(int msqid, int cmd, struct msqid_ds *buf); struct msgbuf { long mtype; /* message type, must be > 0 */ char mtext[128]; /* message data */ }; int main(void) { key_t key; struct msgbuf readbuf = {0}; struct msgbuf sendbuf = {889, "hello world2" }; key = ftok(".", 1); int msgId = msgget(key, IPC_CREAT|0777); if(msgId == -1) { printf("magget error\n"); exit(-1); } msgrcv(msgId, &readbuf, sizeof(readbuf.mtext), 888, 0); printf("readbuf = %s\n", readbuf.mtext); msgsnd(msgId, &sendbuf, sizeof(sendbuf.mtext), 0); msgctl(msgId, IPC_RMID, NULL); return 0; }
六、共享内存
- 说明
1.1 共享内存,指两个或多个进程共享一个给定的存储区。
1.2 ipcs -m 查看系统下已有的共享内存;ipcrm -m shmid可以用来删除共享内存。 - 特点:
2.1 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
2.2 因为多个进程可以同时操作,所以需要进行同步。
2.3 信号量 + 共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。 - 共享内存函数的原型:
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1 int shmget(key_t key, size_t size, int flag); // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1 void *shmat(int shm_id, const void *addr, int flag); // 断开与共享内存的连接:成功返回0,失败返回-1 int shmdt(void *addr); // 控制共享内存的相关信息:成功返回0,失败返回-1 int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
- 源码
write.c
read.c#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h> //int shmget(key_t key, size_t size, int shmflg); //void *shmat(int shmid, const void *shmaddr, int shmflg); //int shmdt(const void *shmaddr); //int shmctl(int shmid, int cmd, struct shmid_ds *buf); int main(void) { key_t key; char *shmaddr; key = ftok(".", 1); int shmId = shmget(key, 1024*4, IPC_CREAT|0666); if(shmId == -1) { printf("creat failed\n"); exit(-1); } shmaddr = (char *)shmat(shmId, 0, 0); strcpy(shmaddr, "jiangyo"); sleep(5); shmdt(shmaddr); shmctl(shmId, IPC_RMID, 0); return 0; }
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <string.h> //int shmget(key_t key, size_t size, int shmflg); //void *shmat(int shmid, const void *shmaddr, int shmflg); //int shmdt(const void *shmaddr); //int shmctl(int shmid, int cmd, struct shmid_ds *buf); int main(void) { key_t key; char *shmaddr; key = ftok(".", 1); int shmId = shmget(key, 1024*4, 0); if(shmId == -1) { printf("creat failed\n"); exit(-1); } shmaddr = (char *)shmat(shmId, 0, 0); printf("buf = %s\n", shmaddr); shmdt(shmaddr); return 0; }
七、信号
- 说明
1.1 对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。
1.2 每个信号都有一个名字和编号,这些名字都以“SIG”开头。我们可以通过kill -l来查看信号的名字以及序号。
1.3 不存在0信号,kill对于0信号有特殊的应用。
1.4 信号相关函数中的初级函数,只涉及到信号捕捉和发送;高级函数的发送函数中可以给结构体赋值,发送信号的同时,把结构体也会携带一起发送,接受函数就能够接受数据。 - 信号的处理:
2.1 信号的处理有三种方法,分别是:忽略、捕捉和默认动作。- 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP);
- 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
- 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。
- 信号处理函数的注册:
//入门版:函数 signal() //高级版:函数 sigaction()
- 信号处理发送函数:
//入门版: kill() //高级版: sigqueue()
- 入门版函数原型:
//接收函数,第二个参数指向信号处理函数 sighandler_t signal(int signum, sighandler_t handler); //发送函数 int kill(pid_t pid, int sig);
- 入门版函数signal()源码
#include <signal.h> #include <stdio.h> //typedef void (*sighandler_t)(int); //sighandler_t signal(int signum, sighandler_t handler); void handler(int num) { switch(num) { case 2: printf("SIGINT\n"); break; case 9: printf("SIGKILL\n"); break; case 10: printf("SIGUSR1\n"); break; default : break; } printf("never quit\n"); } int main(void) { signal(SIGINT, handler); signal(SIGKILL, handler); signal(SIGUSR1, handler); while(1); return 0; }
- 入门版kill()函数源码
#include <signal.h> #include <stdio.h> #include <sys/types.h> //typedef void (*sighandler_t)(int); //sighandler_t signal(int signum, sighandler_t handler); //int kill(pid_t pid, int sig); int main(int argc, char **argv) { int pid, sig; // char cmd[128] = {0}; sig = atoi(argv[1]); pid = atoi(argv[2]); kill(pid, sig); // sprintf(cmd, "kill -%d %d", sig, pid); // system(cmd); return 0; }
- 高级版函数原型
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作 void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用 sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。 int sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据 }; //回调函数句柄sa_handler、sa_sigaction只能任选其一 //我们只需要配置 sa_sigaction以及sa_flags即可。 //接受处理函数中的数据结构体 siginfo_t { int si_signo; /* Signal number */ int si_errno; /* An errno value */ int si_code; /* Signal code */ int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */ pid_t si_pid; /* Sending process ID */ uid_t si_uid; /* Real user ID of sending process */ int si_status; /* Exit value or signal */ clock_t si_utime; /* User time consumed */ clock_t si_stime; /* System time consumed */ sigval_t si_value; /* Signal value */ int si_int; /* POSIX.1b signal */ void *si_ptr; /* POSIX.1b signal */ int si_overrun; /* Timer overrun count; POSIX.1b timers */ int si_timerid; /* Timer ID; POSIX.1b timers */ void *si_addr; /* Memory location which caused fault */ int si_band; /* Band event */ int si_fd; /* File descriptor */ } //发送信号函数 #include <signal.h> int sigqueue(pid_t pid, int sig, const union sigval value); union sigval { int sival_int; void *sival_ptr; };
- 高级版函数sigqueue()源码
#include <stdio.h> #include <signal.h> #include <sys/types.h> #include <unistd.h> //int sigqueue(pid_t pid, int sig, const union sigval value); int main(int argc, char **argv) { int num; pid_t pid; union sigval sig; sig.sival_int = 100; num = atoi(argv[1]); pid = atoi(argv[2]); sigqueue(pid, num, sig); printf("send pid = %d\n", getpid()); return 0; }
- 高级版函数sigaction()源码
#include <stdio.h> #include <signal.h> #include <sys/types.h> #include <unistd.h> //int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); void handler(int num, siginfo_t *info, void *buf) { printf("sig num = %d\n", num); if(buf != NULL) { printf("data = %d\n", info->si_int); printf("send pid = %d\n", info->si_pid); } } int main(void) { struct sigaction act = {0}; printf("read pid = %d\n", getpid()); act.sa_sigaction = handler; act.sa_flags = SA_SIGINFO; sigaction(SIGUSR1, &act, NULL); while(1); return 0; }
八、信号量
- 说明
信号量与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。 - 特点
2.1 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
2.2 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
2.3 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
2.4 支持信号量组 - 信号量的函数原型:
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1 //num_sems表示创建几个信号量,sem_flags权限 int semget(key_t key, int num_sems, int sem_flags); // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1 //semid表示semget函数的返回值,numops操作几个信号量 int semop(int semid, struct sembuf semoparray[], size_t numops); // 控制信号量的相关信息 int semctl(int semid, int sem_num, int cmd, ...);
- 当 semget 创建新的信号量集合时,必须指定集合中信号量的个数(即 num_sems),通常为 1; 如果是引用一个现有的集合,则将 num_sems 指定为 0 。
- 在 semop 函数中,sembuf 结构的定义如下:
struct sembuf { short sem_num; // 信号量组中对应的序号,0~sem_nums-1 short sem_op; // 信号量值在一次操作中的改变量 short sem_flg; // IPC_NOWAIT, SEM_UNDO }
- 在 semctl 函数中的命令有多种,这里就说两个常用的:
- SETVAL:用于初始化信号量为一个已知的值。
- IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
- 源码
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <unistd.h> //int semget(key_t key, int nsems, int semflg); //int semctl(int semid, int semnum, int cmd, ...); //int semop(int semid, struct sembuf *sops, unsigned nsops); 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 sop = {0}; sop.sem_num = 0; 信号量组中对应的序号,0~sem_nums-1 sop.sem_op = -1; sop.sem_flg = SEM_UNDO; // IPC_NOWAIT, SEM_UNDO semop(semid, &sop, 1); printf("get key\n"); } void vPutBackKey(int semid) { struct sembuf sop = {0}; sop.sem_num = 0; 信号量组中对应的序号,0~sem_nums-1 sop.sem_op = 1; sop.sem_flg = SEM_UNDO; // IPC_NOWAIT, SEM_UNDO semop(semid, &sop, 1); printf("put key\n"); } int main(void) { key_t key; int semId; if((key == ftok(".",6)) < 0) { printf("ftok error\n"); } semId = semget(key , 1, IPC_CREAT|0666);//创造钥匙,数量为1 union semun semm; semm.val = 0;//初始状态为没有钥匙 //0:control 1 key semctl(semId, 0, SETVAL, semm); pid_t pid = fork(); if(pid>0){ pGetKey(semId); printf("this is father\n"); vPutBackKey(semId); semctl(semId, 0, IPC_RMID); } else if(pid == 0) { printf("this is child\n"); vPutBackKey(semId); } else { printf("creat error\n"); } return 0; }
九、进程间通信方式总结:
- 管道:速度慢,容量有限,只有父子进程能通讯;
- FIFO:任何进程间都能通讯,但速度慢;
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;
- 共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题;
- 信号:有入门版和高级版两种,区别在于入门版注重动作,高级版可以传递消息。只有在父子进程或者是共享内存中,才可以发送字符串消息;
- 信号量:不能传递复杂消息,只能用来同步。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。