当前位置: 首页 > news >正文

Linux进程间通信(IPC)

一、IPC 通信原理

通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。如下图:

进程间通信方式

Linux 进程间基本的通信方式主要有:管道(pipe) (包括匿名管道和命名管道)、信号(signal)、消息队列(queue)、共享内存、信号量和Socket。 

二、管道

管道的实质是一个内核缓冲区(调用 pipe 函数来开辟),管道的作用正如其名,需要通信的两个进程在管道的两端,进程利用管道传递信息。管道对于管道两端的进程而言,就是一个文件,但是这个文件比较特殊,它不属于文件系统并且只存在于内存中。 Linux一切皆文件,操作系统为管道提供操作的方法:文件操作,用 fork 来共享管道原理。

管道依据是否有名字分为匿名管道和命名管道(有名管道),这两种管道有一定的区别。

匿名管道有几个重要的限制:

  1. 管道是半双工的,数据只能在一个方向上流动,A进程传给B进程,不能反向传递
  2. 管道只能用于父子进程或兄弟进程之间的通信,即具有亲缘关系的进程。

 命名管道允许没有亲缘关系的进程进行通信。命名管道不同于匿名管道之处在于它提供了一个路径名与之关联,这样一个进程即使与创建有名管道的进程不存在亲缘关系,只要可以访问该路径,就能通过有名管道互相通信。

pipe 函数接受一个参数,是包含两个整数的数组,如果调用成功,会通过 pipefd[] 传出给用户程序两个文件描述符,需要注意 pipefd[0] 指向管道的读端, pipefd[1] 指向管道的写端,那么此时这个管道对于用户程序就是一个文件,可以通过 read(pipefd [0]);或者 write(pipefd [1]) 进行操作。pipe 函数调用成功返回 0,否则返回 -1.

通过管道进行通信的步骤:

  • 父进程创建管道,得到两个文件描述符指向管道的两端

 

 

  • 利用fork函数创建出子进程,则子进程也得到两个文件描述符指向同一管道

 

  • 父进程关闭读端(pipe[0]),子进程关闭写端pipe[1],则此时父进程可以往管道中进行写操作,子进程可以从管道中读,从而实现了通过管道的进程间通信。

 

管道的特点:

  • 只能单向通信

两个文件描述符,用一个,另一个不用,不用的文件描述符就要 close

  • 只能血缘关系的进程进行通信

  • 依赖于文件系统

  • 生命周期随进程

  • 面向字节流的服务

        面向字节流:数据无规则,没有明显边界,收发数据比较灵活:对于用户态,可以一次性发送也可以分次发送,当然接受数据也如此;而面向数据报:数据有明显边界,数据只能整条接受 

  • 管道内部提供了同步机制

临界资源: 大家都能访问到的共享资源

临界区: 对临界资源进行操作的代码

同步: 临界资源访问的可控时序性(一个操作完另一个才可以操作)

互斥: 对临界资源同一时间的唯一访问性(保护临界资源安全)

注意:

因为管道通信是单向的,在上面的例子中我们是通过子进程写父进程来读,如果想要同时父进程写而子进程来读,就需要再打开另外的管道;

管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道的件描述符。 上面的例子是父进程把文件描述符传给子进程之后父子进程之 间通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信, 总之 需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。

 四个特殊情况:

  1. 如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样

  2. 如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。

  3. 如果所有指向管道读端的文件描述符都关闭了,这时有进程指向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。

  4. 如果有指向管道读端的文件描述符没关闭,而持有管道写端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再write会阻塞,直到管道中有空位置了才写入数据并返回。

无名管道的复制: 

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc, char const *argv[])
{int fd[2] = {0};int ret = pipe(fd);if(-1 == ret){perror("pipe");return 1;}pid_t pid = fork();if(pid > 0){close(fd[0]);int srcfd = open("/home/linux/1.jpg", O_RDONLY);while(1){char s1[4096] = {0};int ret = read(srcfd, s1, sizeof(s1));write(fd[1], s1, ret);}close(fd[1]);}else if(0 == pid){close(fd[1]);int dstfd = open("2.jpg", O_WRONLY | O_CREAT | O_TRUNC, 0666);while(1){char s[4096] = {0};int ret = read(fd[0], s, sizeof(s));if(0 == ret){break;}write(dstfd, s, ret);}close(dstfd);close(fd[0]);}else{perror("fork");return 1;}return 0;
}

三、命名管道FIFO

 在管道中,只有具有血缘关系的进程才能进行通信,对于后来的命名管道,就解决了这个问题。FIFO 不同于管道之处在于它提供一个路径名与之关联,以 FIFO 的文件形式存储于文件系统中。命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过 FIFO 相互通信。值得注意的是, FIFO (first input first output) 总是按照先进先出的原则工作,第一个被写入的数据将首先从管道中读出。

命名管道的创建

创建命名管道的系统函数有两个: mknod 和 mkfifo。两个函数均定义在头文件 sys/stat.h,
函数原型如下:

#include <sys/types.h>
#include <sys/stat.h>
int mknod(const char *path,mode_t mod,dev_t dev);
int mkfifo(const char *path,mode_t mode);

函数 mknod 参数中 path 为创建的命名管道的全路径名: mod 为创建的命名管道的模指明其存取权限; dev 为设备值,该值取决于文件创建的种类,它只在创建设备文件时才会用到。这两个函数调用成功都返回 0,失败都返回 -1。

命名管道打开特性:

  1. 如果用只读打开命名管道,open 函数将阻塞等待直至有其他进程以写的方式打开这个命名管道,如果没有进程以写的方式发开这个命名管道,程序将停在此处

  2. 如果用只写打开命名管道,open 函数将阻塞等到直至有其他进程以读的方式打开这个命名管道,如果没有进程以读的方式发开这个命名管道,程序将停在此处;

  3. 如果用读写打开命名管道,则不会阻塞(但是管道是单向)

有名管道的复制: 

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc, char const *argv[])
{int ret = mkfifo("fifo", 0666);if(-1 == ret){if(EEXIST != errno){perror("mkfifo");return 1;}}int fd = open("fifo", O_WRONLY);if(-1 == fd){perror("open fifo");return 1;}int srcfd = open("/home/linux/1.jpg", O_RDONLY);while(1){char s1[4096] = {0};int ret = read(srcfd, s1, sizeof(s1));if(0 == ret){break;}write(fd, s1, ret);}close(srcfd);close(fd);return 0;
}
int main(int argc, char const *argv[])
{int ret = mkfifo("fifo", 0666);if(-1 == ret){if(EEXIST != errno){perror("mkfifo");return 1;}}int fd = open("fifo", O_RDONLY);if(-1 == fd){perror("open fifo");return 1;}int dstfd = open("3.jpg", O_WRONLY | O_CREAT | O_TRUNC, 0666);while(1){char s[4096] = {0};int ret = read(fd, s, sizeof(s));if(0 == ret){break;}write(dstfd, s, ret);}close(dstfd);close(fd);remove("/home/linux/DS/proc/03ipc/06fifo/fifo");return 0;
}

四、信号通信

1. 概念:

  1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式

  2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。

  3)如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被 取消时才被传递给进程。

2.用户进程对信号的响应方式:

  1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。

  2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。

  3)执行缺省操作:Linux对每种信号都规定了默认操作

3.信号:

  SIGINT:ctrl+c 终止信号

  SIGQUIT:ctrl+\ 终止信号

  SIGTSTP:ctrl+z 暂停信号

  SIGALRM:闹钟信号 收到此信号后定时结束,结束进程

  SIGCHLD:子进程状态改变,父进程收到信号

  SIGKILL:杀死信号

4.相关函数:

1)int kill(pid_t pid, int sig);

  功能:信号发送
  参数:pid:指定进程
  sig:要发送的信号
  返回值:成功 0;失败 -1

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
// ./a.out 1000 9
int main(int argc, char **argv)
{if(argc < 3){printf("usage: ./a.out pid signum\n");return 1;}pid_t pid = atoi(argv[1]);int num = atoi(argv[2]);int ret = kill(pid, num);if(-1 == ret){perror("kill");return 1;}return 0;
}
2)int raise(int sig);

  功能:进程向自己发送信号
  参数:sig:信号
  返回值:成功 0;失败 -1

3)unsigned int alarm(unsigned int seconds)

  功能:在进程中设置一个定时器
  参数:seconds:定时时间,单位为秒
  返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替

4)int pause(void);

  功能:用于将调用进程挂起直到收到信号为止。

5)void (*signal(int signum, void (*handler)(int)))(int);

  或者:
  typedef void (*sighandler_t)(int);
  sighandler_t signal(int signum, sighandler_t handler);
  功能:信号处理函数
  参数:signum:要处理的信号//不能是SIGKILL和SIGSTOP
    handler:SIG_IGN:忽略该信号。
    SIG_DFL:采用系统默认方式处理信号。
    自定义的信号处理函数指针
  返回值:成功:设置之前的信号处理方式;失败:SIG_ERR

signal_alarm 

int flag = 0;
void handle(int num)
{flag = 1;
}
int main(int argc, char **argv)
{signal(SIGALRM, handle);alarm(5);while(1){if(0 == flag){printf("sleep...\n");}else{printf("working...\n");}sleep(1);}system("pause");return 0;
}

 signal_cont

void handle(int num)
{// printf("111111111\n");
}
int main(int argc, char const *argv[])
{signal(SIGCONT,handle);int i = 0;while(1){printf("work... pid = %d\n",getpid());sleep(1);i++;if(i == 5){pause();}}return 0;
}

siguser

void handle1(int num)
{static int i = 0;printf("老爸叫你...\n");i++;if(i ==3){signal(SIGUSR1,SIG_IGN); //忽略信号}}
void handle2(int num)
{static int i = 0;printf("老妈叫你...\n");i++;if(i ==3){signal(SIGUSR2,SIG_DFL); //回复默认信号}}int	main(int argc, char **argv)
{signal(SIGUSR1,handle1);signal(SIGUSR2,handle2);while(1){printf("i'm playing pid:%d\n",getpid());sleep(1);}system("pause");return 0;
}

相关文章:

  • Ubuntu系统 | 本地部署ollama+deepseek
  • 微软PowerBI考试 PL300-Power BI 入门
  • 自驾总结_Localization
  • 免费批量文件重命名软件
  • [蓝桥杯]最大化股票交易的利润
  • 湖北理元理律师事务所:系统性债务化解中的法律技术革新
  • 大模型分布式训练笔记(基于accelerate+deepspeed分布式训练解决方案)
  • 【Connected Paper使用以及如何多次使用教程分享】
  • 机器学习——放回抽样
  • 【Typst】4.导入、包含和读取
  • HTTP连接管理——短连接,长连接,HTTP 流水线
  • 二维 根据矩阵变换计算缩放比例
  • 49套夏日小清新计划总结日系卡通ppt模板
  • 什么是C语言块级变量
  • 从 Docker 到 Containerd:Kubernetes 容器运行时迁移实战指南
  • Alita:通过 MCP 实现自主进化的通用 AI 代理
  • 星敏感器:卫星姿态测量的“星空导航仪”
  • 三极管和MOS的三种状态命名的区别
  • 2024-2025-2-《移动机器人设计与实践》-复习资料-8……
  • 小家电外贸出口新利器:WD8001低成本风扇智能控制方案全解析
  • 阀门专业网站建设/关键词是怎么排名的
  • 长沙找人做网站/青岛网站权重提升
  • 通辽网站开发/软文推送
  • 东莞广告公司东莞网站建设价格/品牌如何推广
  • 微信小程序开发官网网址/企业网站优化报告
  • pyhton做网站/培训心得体会总结