应用间通信(一):详解Linux进程IPC
进程之间是独立的、隔离的,使得应用程序之间绝对不可以互相"侵犯"各自的领地。
但,应用程序之间有时是需要互相通信,相互写作,才能完成相关的功能,这就不得不由操作系统介入,实现一种通信机制。在这种通信机制的监管下,让应用程序之间实现通信。
Linux实现了诸如管道、信号、消息队列、共享内存,这就是Linux进程IPC.
管道
在Linux中管道作为最古老的通信方式,它能把一个进程生产的数据输送到另一个进程。
比如,在shell中输入ls -al /|wc -l
命令来统计根目录下有多少文件和目录。该命令中的"|"
就是让shell创建ls进程后建立一个管道,连接到wc
进程,使用ls的输出经由管道输入给wc.由于ls
输出的是文本行,一个目录或者一个文件就占用一行,wc
通过统计文本行数就能知道有多少目录和文件。
手动建立一个管道,代码如下:
int main()
{
pid_t pid;
int rets;
int fd[2];
char r_buf[1024] = {0};
char w_buf[1024] = {0};
// 把字符串格式化写入w_buf数组中
sprintf(w_buf, "这是父进程 id = %d\n", getpid());
// 建立管道
if(pipe(fd) < 0)
{
perror("建立管道失败\n");
}
// 建立子进程
pid = fork();
if(pid > 0)
{
// 写入管道
write(fd[1], w_buf, strlen(w_buf));
// 等待子进程退出
wait(&rets);
}
else if(pid == 0)
{
// 新进程
printf("这是子进程 id = %d\n", getpid());
// 读取管道
read(fd[0], r_buf, strlen(w_buf));
printf("管道输出:%s\n", r_buf);
}
return 0;
}
上面的代码是一份代码,两个进程,父进程经过 fork 产生了子进程,子进程从 pid==0
开始运行。其中非常重要的是调用 pipe 函数
,作用是建立一个管道。函数参数 fd 是文件句柄数组,其中 fd[0]的句柄表示读端,而 fd[1]句柄表示写端。
运行结果如下:
可看出,子进程通过管道获取了父进程写入的信息,但有没有想过,为什么可以通过pipe
和fork
就可以在父子进程建立管道进行通信呢?
可以将管道想象成一个只存在于内存中的、共享的特殊文件。不过该文件有两个描述符,一个是专用于读,一个专用于写。
上图中 pipe 函数会使 Linux 在父进程中建立一个文件和两个 file 结构,分别用于读取和写入。调用 fork 之后,由于复制了父进程的数据结构,所以子进程也具备了这两个 file 结构,并且都指向同一个 inode 结构。inode 结构在 Linux 中代表一个文件,这个 inode 会分配一些内存页面来缓存数据。但对于管道文件来说,这些页面中的数据不会写入到磁盘。
这也是为什么在应用程序中管道是用文件句柄索引,并使用文件读写函数来读写管道,因为管道本质上就是一个内存中的文件。
和读写文件一样,读写管道也有相应的规则:当管道中没有数据可读时,进程调用 read 时会阻塞,即进程暂停执行,一直等到管道有数据写入为止;当管道中的数据被写满的时候,进程调用 write 时阻塞,直到有其它进程从管道中读走数据。
如果所有管道写入端对应的文件句柄被关闭,则进程调用 read 时将返回 0;如果所有管道的读取端对应的文件句柄被关闭,则会调用 write,从而产生 SIGPIPE 信号,这可能导致调用 write 进程退出。这些规则由 Linux 内核维护,应用开发人员不用操心。
如果要写入的数据量小于管道内部缓冲时,Linux 内核将保证这次写入操作的原子性。但是当要写入的数据量大于管道内部缓冲时,Linux 内核将不再保证此次写入操作的原子性,可能会分批次写入。
这些读写规则,都是基于管道读写端是阻塞状态下的情况,你可以调用 fcntl 调用
,把管道的读写端设置非阻塞状态。这样调用 write 和 read 不满足条件时,将直接返回相应的错误码,而不是阻塞进程。
管道是一种非常简单的通信机制,由于数据在其中像水一样,从水管的一端流动到另一端,故而得名管道。注意,管道只能从一端流向另一端,不能同时对流。之所以说管道简单,正是因为它是一种基于两个进程间的共享内存文件实现的,可以继承文件操作的 api 接口,这也符合 Linux 系统一切皆文件的设计思想。
信号
Linux 信号,也是种古老的进程间通信方式,不过,这里的信号我们不能按照字面意思来理解。Linux 信号是一种异步事件通知机制,类似于计算机底层的硬件中断。
简单来说,信号是 Linux 操作系统为进程设计的一种软件中断机制,用来通知进程发生了异步事件
。事件来源可以是另一个进程,这使得进程与进程之间可以互相发送信号;事件来源也可以是 Linux 内核本身,因为某些内部事件而给进程发送信号,通知进程发生了某个事件。
从进程执行的行为来说,信号能打断进程当前正在运行的代码,转而执行另一段代码。信号来临的时间和信号会不会来临,对于进程而言是不可预知的,这说明了信号的异步特性。
举例:
用定时器在既定的时间点产生信号,发送给当前运行的进程,使进程结束运行。代码如下所示:
void handle_timer(int signum, siginfo_t *info, void *ucontext)
{
printf("handle_timer 信号码:%d\n", signum);
printf("进程:%d 退出!\n", getpid());
// 正常退出进程
exit(0);
return;
}
int main()
{
struct sigaction sig;
// 设置信号处理回调函数
sig.sa_sigaction = handle_timer;
sig.sa_flags = SA_SIGINFO;
// 安装定时器信号
sigaction(SIGALRM, &sig, NULL);
// 设置4秒后产生信号SIGALRM信号
alarm(4);
while(1)
{
;// 死循环防止进程退出
}
return 0;
}
第一步,main 函数中通过 sigaction 结构
设置相关信号,例如信号处理回调函数和一个信号标志。
第二步,安装信号,通过 sigaction
函数把信号信息传递给 Linux 内核,Linux 内核会在这个进程上,根据信号信息安装好信号。
第三步,产生信号,alarm 函数会让 Linux 内核设置一个定时器,到了特定的时间点后,内核发现时间过期了就会给进程发出一个 SIGALRM 信号,由 Linux 内核查看该进程是否安装了信号处理函数,以及是否屏蔽了该信号。确定之后,Linux 内核会保存进程当前上下文,然后构建一个执行信号处理函数的栈帧,让进程返回到信号处理函数运行。
运行结果:
可以看到,程序运行起来等待 4 秒后,内核产生了 SIGALRM 信号,然后开始执行 handle_timer 函数。请注意,我们在 main 函数没有调用 handle_timer 函数,它是由内核异步调用的。在 handle_timer 函数中输出了信号码,然后就调用 exit 退出进程了。
信号码是什么?
信号码是一个整数,是一种信号的标识,代表某一种信号。SIGALRM
定义为14.可以使用kill -l
命令查看下Linux系统支持的全部信号。
常用的信号如下:
上面都是 Linux 的标准信号,它们大多数来源于键盘输入、硬件故障、系统调用、应用程序自身的非法运算。一旦信号产生了,进程就会有三种选择:忽略、捕捉、执行默认操作。其实大多数应用开发者都采用忽略信号或者执行信号默认动作,这是一种“信号来了,我不管”的姿态。
一般信号的默认动作就是忽略
把前面那个“闹钟”程序升一下级。代码如下所示:
static pid_t subid;
void handle_sigusr1(int signum, siginfo_t *info, void *ucontext)
{
printf("handle_sigusr1 信号码:%d\n", signum);
//判断是否有数据
if (ucontext != NULL)
{
//保存发送过来的信息
printf("传递过来的子进程ID:%d\n", info->si_int);
printf("发送信号的父进程ID:%d\n", info->si_pid);
// 接收数据
printf("对比传递过来的子进程ID:%d == Getpid:%d\n", info->si_value.sival_int, getpid());
}
// 退出进程
exit(0);
return;
}
int subprocmain()
{
struct sigaction sig;
// 设置信号处理函数
sig.sa_sigaction = handle_sigusr1;
sig.sa_flags = SA_SIGINFO;
// 安装信号
sigaction(SIGUSR1, &sig, NULL);
// 防止子进程退出
while (1)
{
pause(); // 进程输入睡眠,等待任一信号到来并唤醒进程
}
return 0;
}
void handle_timer(int signum, siginfo_t *info, void *ucontext)
{
printf("handle_timer 信号码:%d\n", signum);
union sigval value;
// 发送数据,也可以发送指针
value.sival_int = subid; // 子进程的id
// 调用sigqueue,向子进程发出SIGUSR1信号
sigqueue(value.sival_int, SIGUSR1, value);
return;
}
int main()
{
pid_t pid;
// 建立子进程
pid = fork();
if (pid > 0)
{
// 记录新建子进程的id
subid = pid;
struct sigaction sig;
// 设置信号处理函数
sig.sa_sigaction = handle_timer;
sig.sa_flags = SA_SIGINFO;
// 安装信号
sigaction(SIGALRM, &sig, NULL);
alarm(4);// 4秒后发出SIGALRM信号
while (1)
{
pause(); // 进程输入睡眠,等待任一信号到来并唤醒进程
}
}
else if (pid == 0)
{
// 新进程
subprocmain();
}
return 0;
}
上面的代码逻辑很简单:首先我们在主进程中调用 fork 建立一个子进程。接着子进程开始执行 subprocmain 函数,并在其中安装了 SIGUSR1 信号处理函数,让子进程进入睡眠。4 秒钟后主进程产生了 SIGALRM 信号,并执行了其处理函数 handle_timer,在该函数中调用 sigqueue 函数,向子进程发出 SIGUSR1 信号,同时传递了相关信息。最后,子进程执行 handle_sigusr1 函数处理了 SIGUSR1 信号,打印相应信息后退出。
运行结果如下:
上图输出的结果,正确地展示了两个信号的处理过程:第一个 SIGALRM 信号是 Linux 内核中的定时器产生;而第二个 SIGUSR1 信号是我们调用 sigqueue 函数手动产生的。
sigqueue 的函数原型如下所示:
typedef union sigval {
int sival_int;
void *sival_ptr;
} sigval_t;
// pid 发送信号给哪个进程,就是哪个进程id
// sig 发送信号的信号码
// 附加value值(整数或指针)
// 函数成功返回0,失败返回-1
int sigqueue(pid_t pid, int sig, const union sigval value);
总结一下
信号是 Linux 内核基于一些特定的事件,并且这些事件要让进程感知到,从而实现的一种内核与进程之间、进程与进程之间的异步通信机制。
一幅图来简单了解一下 Linux 内核对信号机制的实现,如下所示:
无论是硬件事件还是系统调用触发信号,都会演变成设置进程数据结构 task_struct
中 pending
对应的位。这其中每个位对应一个信号,设置了 pending 中的位还不够,我们还要看一看,blocked 中对应的位是不是也被设置了。
如果 blocked 中对应的位也被设置了,就不能触发信号(这是给信号提供一种阻塞策略,对于有些信号没有用,如 SIGKILL、SIGSTOP 等);否则就会触发该位对应的 action,根据其中的标志位查看是否捕获信号,进而调用其中 sa_handler 对应的函数。
应用间通信(一):详解Linux进程IPC的更多相关文章
- Linux进程上下文切换过程context_switch详解--Linux进程的管理与调度(二十一)
1 前景回顾 1.1 Linux的调度器组成 2个调度器 可以用两种方法来激活调度 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测 ...
- 详解Linux进程(作业)的查看和杀死
目录: 引入进程 进程 线程 PS命令 TOP命令 其他查看进程命令 进程的优先级 作业控制机制 kill命令 一.引入进程 1.内存划分为:用户空间和内核空间 1.在用户空间里运行的进程,就是用户进 ...
- Linux内核线程kernel thread详解--Linux进程的管理与调度(十)【转】
转自:http://blog.csdn.net/gatieme/article/details/51589205 日期 内核版本 架构 作者 GitHub CSDN 2016-06-02 Linux- ...
- Linux的命名空间详解--Linux进程的管理与调度(二)【转】
Linux Namespaces机制提供一种资源隔离方案. PID,IPC,Network等系统资源不再是全局性的,而是属于特定的Namespace.每个Namespace里面的资源对其他Namesp ...
- Linux的命名空间详解--Linux进程的管理与调度(二)
转自:http://blog.csdn.net/gatieme/article/details/51383322 日期 内核版本 架构 作者 GitHub CSDN 2016-05-12 Linux- ...
- Linux内核线程kernel thread详解--Linux进程的管理与调度(十)
内核线程 为什么需要内核线程 Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求). 内核需要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的. 内核线程 ...
- Linux下进程的创建过程分析(_do_fork do_fork详解)--Linux进程的管理与调度(八)
Unix标准的复制进程的系统调用时fork(即分叉),但是Linux,BSD等操作系统并不止实现这一个,确切的说linux实现了三个,fork,vfork,clone(确切说vfork创造出来的是轻量 ...
- ELF文件的加载过程(load_elf_binary函数详解)--Linux进程的管理与调度(十三)
加载和动态链接 从编译/链接和运行的角度看,应用程序和库程序的连接有两种方式. 一种是固定的.静态的连接,就是把需要用到的库函数的目标代码(二进制)代码从程序库中抽取出来,链接进应用软件的目标映像中: ...
- Linux进程描述符task_struct结构体详解--Linux进程的管理与调度(一)【转】
Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息.它定义在include/linux/sched.h文件中. 谈到task_str ...
- Linux下ps命令详解 Linux下ps命令的详细使用方法
http://www.jb51.net/LINUXjishu/56578.html Linux下的ps命令比较常用 Linux下ps命令详解Linux上进程有5种状态:1. 运行(正在运行或在运行队列 ...
随机推荐
- docker - [08] Portainer可视化面板安装
Docker图形化界面管理工具 一.运行容器 同时下载和使用镜像运行容器 docker run -d -p 8088:9000 \ --restart=always -v /var/run/docke ...
- Shell - [11] 开源Apache Zookeeper集群启停脚本
一.集群角色部署 当前有Zookeeper集群如下 主机名 ctos79-01 ctos79-02 ctos79-03 Zookeeper ○ ○ ○ 二.脚本使用 三.脚本内容 #!/bin/bas ...
- 【vulhub】redis CVE-2022-0543(redis沙盒逃逸)
渗透环境 攻击机: IP: 192.168.66.130(Kali) 漏洞收录于:vulhub/redis/CVE-2022-0543 涉及知识点:redis沙盒逃逸 漏洞详情 受影响的系统: 仅 ...
- 在ubuntu系统下,安装opencv各个版本
要在Linux系统上安装OpenCV库,你可以通过包管理器(如apt)来安装.以下是详细的步骤,包括如何在/usr/local/lib或/usr/lib/x86_64-linux-gnu目录下安装Op ...
- Web前端入门第 8 问:HTML <!DOCTYPE> 申明有何用处?如果没有此申明有什么问题?
HELLO,这里是大熊学习前端开发的入门笔记. 本系列笔记基于 windows 系统. 先电脑端浏览器打开任何一个网页,比如百度. 再用 ctrl + u 快捷键即可查看源码,瞅瞅第一行代码,是不是都 ...
- mac 源码编译安装php8.3.9
前提条件 确保你已经安装了 Homebrew 和 Xcode Command Line Tools.你可以通过以下命令安装它们: /bin/zsh -c "$(curl -fsSL http ...
- Ubuntu 下查看当前用户
博客地址:https://www.cnblogs.com/zylyehuo/ 在终端执行以下命令 whoami
- SpringBoot集成LDAP认证登录
Maven依赖 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ...
- 安装卸载GNOME
只需要三步:sudo yum -y groups install "GNOME Desktop"sudo systemctl set-default graphical.targe ...
- PMP学习记录
本人在2020年12月已经顺利拿到PMP证书. 第一次听说PMP证书是2016年,一个同事说考试通过拿到了PMP证书,当时对PMP不是很了解.也未作深入了解,当时认为俺是做技术的,这个证书没啥用.O( ...