Fork me on GitHub

Linux编程之进程间通信

进程通信概念

进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。那么释放的资源可能是其他进程需要的,然而进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。

进程通信应用场景

  • 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

  • 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

  • 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信的方式

管道(pipe)

1

管道实现细节
在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如下图
2

匿名管道

仅仅适用于具有亲缘关系的进程间通信,因为匿名管道其它进程根本找不到,因此也就没有办法通信,所以只能通过子进程复制父进程的方法,让子进程能够访问相同的管道,来实现通信。
(管道的操作:io操作—文件描述符)

命名管道

有名字:体现在文件系统可见性,因为其它进程都能看见这个管道文件,因此都能打开可以用于任意(本机)进程间通信
(单向通信)所以一个管道使用的时候就必须确定数据流向,但是不能一创建就确 定,因为我们不确定谁读谁写。 因此,操作系统提供两个描述符来供
使用,一个读一个写,这样的确定方向就是将对应的一段关闭掉就可以,这样操作系统就把方向的控制权交给用户了
接口:pipe(int fd[2])fd[0]— 读 fd[1]—写

信号量(semophore)

信号量并不是用来数据传输的,而是用来进程控制,是解决进程间同步与互斥问题
是一个具有等待队列的计数器
释放资源+1,
获取资源-1,
当计数器的值不大于0,意味着没有资源,想要获取信号量资源(计数器-1)的就需要等待
同步:如果现在没有资源,等待,等待别人释放资源,别人释放资源后会通知等待的人
互斥:一元信号量实现互斥(计数器是0或 1)

消息队列(message queue)

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 它通常在内核中创建一个消息队列,其它的所有进程都可以通过相同的IPC_KEY 打开消息队列
这时候既可以向消息队列中放数据,也可以从中拿数据,但是这样的数据就有可能拿错了,拿到的不是自己的数据,因此消息队列中能够放的数据是有类型的数据块,并且读写的时候只能按消息块来发送/接收。

信号(sinal)

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
它是通过软中断的方式来进行的,信号产生之后第一时间也不是直接处理,而是先存储下来,处理信号
为了通知进程发生了某个事件,因为事件比较紧急,因此会打断当前进程正在执行的工作,然后去准备处理事件,事件处理完毕后进程回到原先运行的位置继续运行

信号的产生->信号的注册->注销信号->信号的处理
Linux下有62种信号,kill -l查看
信号分两类:普通信号(不可靠信号1~31)(非实时信号)
可靠信号(34~64)(实时信号)

信号的产生:

  • 1.硬件中断产生 Ctrl+C
  • 2.程序异常 SIGFPE SIGSEGV
  • 3.软件条件产生
    1
    2
    3
    4
    5
    6
    7
    int kill(pid_t pid,int sig);
    kill(getpid(),SIGINT);
    int raise();
    raise(SIGTERM);
    void abort(void);
    sigqueue
    alarm(3); // 在3秒后向进程发送SIGALRM信号,返回值会取消上一次的定时器,并且返回上一次定时器剩余时间

核心转储(core dump)
保存当前程序运行的数据以及调用栈信息,用于错误原因定位调试。如果程序运行出现错误,可以直接通过core文件来gdb调试(有些错误可能偶然发生) coredump默认关闭:隐私安全/资源占用

信号的注册:
pcb中有一个信号结构体,信号注册/进程发送信号,就是修改这个进程pcb中关于信号的pending位图,将相应的信号为置1;

1
2
3
4
struct signal
{
sigset_t pending;
}

信号的阻塞:暂时不处理信号(阻止信号的递达),并不是不接收信号,
pcb中还有一个信号位图block,要阻止一个信号就是修改pcb中关于信号的block位图,将相应的信号位置1,这个位置就像是一个备注说明如果接收到这
个信号暂时不处理

信号未决:这是一种状态,信号从注册成功到信号递达之间

信号的注销:
就是从pending集合中将即将处理的信号相应位置0(从pcb的pending集合中移除)
非可靠信号注册就是将相应pending位图置1,然后添加一个sigqueue结构到链表中,之后如果有相应信号到来,一看位图已经 置1那么就不做任何操作,意味着后来的信号在前一个信号未处理之前不会重复注册,代表丢了!!!

可靠信号就是不管有没有注册都要置1,并且添加结点到链表中,所以不会丢信号。
非可靠注销就是删除链表结点,相应位图置0.
可靠信号删除结点,判断是否有相同信号结点,如果没有则位图置0,如果有则置1;

信号的递达(信号的处理):

  • 默认操作——安装操作系统中对信号事件的既定处理方式
  • 忽略操作——直接将信号丢掉
  • 自定义处理—–用户自定义事件处理方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    signal接口
    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    signum:信号的编号
    handler:处理方式
    SIG_IGN 忽略
    SIG_DFL 默认

信号的捕捉流程:
信号并不是立即处理,而是选择一个合适的时机处理,合适的时机就是当前程序从==内核态==切换到==用户态==的时候
程序如何从内核态切换到用户态:发起系统调用,程序异常,中断时
信号是当我们发起系统调用/程序异常/中断当前程序从用户态切换到内核态,去处理这些事情,处理完毕后,要从内核态返回用户态,但是在返回之前会看一下是否有信号需要被处理,如果有,就处理信号(切换到用户态执行信号的自定义处理方式),处理完毕后再次返回内核态,判断如果没有信号要处理了就调用sys_sigreturn返回用户态(我们程序之前的运行位置)

1
2
3
4
5
6
sigaddset
sigdelst
sigfillset
sigprocmask信号阻塞接口,阻塞函数,向block添加信号
sigismember
sigpending

struct sigqueue

操作系统如何通知父进程说子进程退出了?
信号:SIGCHLD -17号信号
用户自定义信号:SIGCHLD -17处理方式,相当于提前告诉进程,当接收到这个信号时使用waitpid,这样就不用一直等待

共享内存(shared memory)

是进程间通信速度最快的方式
其它的进程间通信方式,都会涉及到将用户空间的数据拷贝到内核空间(因为公共缓冲区都在内核空间),这是两步操作(拷入和拷出)
而共享内存的原理是多个进程将同一块物理内存映射到自己的虚拟地址空间,以这种方式实现数据共享,操作这个虚拟地址就是操作这个物理内存,相较于其它通信方式,少了两步用户空间和内核空间的拷贝过程,因此速度最快
共享内存操作步骤: 共享内存的生命周期随内核
1.创建/打开一块共享内存
2.将这块共享空间映射到自己的虚拟地址空间
3.各种内存操作
4.解除映射关系
5.删除共享内存

套接字(socket)

套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。因此被广泛应用于网络通信中。

进程间通信的实现

匿名管道通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*  这是一个匿名管道的demo
* 匿名管道操作:
* 创建:pipe
* 读写:read write
* 匿名管道仅能用于具有亲缘关系的进程间通信
* 创建匿名管道必须在创建子进程之前,否则子进程将无法复制
*/

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
pid_t pid;
int pipefd[2] = {0};

if (pipe(pipefd) < 0) {
perror("pipe error");
return -1;
}

pid = fork();
if (pid < 0) {
perror("fork error");
return -1;
}else if (pid == 0) {
//子进程 读
sleep(3);
close(pipefd[1]);
char buff[1024] = {0};
read(pipefd[0], buff, 11);
printf("child:%s\n", buff);
read(pipefd[0], buff, 11);
printf("child:%s\n", buff);
close(pipefd[0]);
}else {
//父进程 写
close(pipefd[0]);
char *ptr = "hello world";
write(pipefd[1], ptr, strlen(ptr));
write(pipefd[1], ptr, strlen(ptr));
close(pipefd[1]);
}
return 0;
}

命名管道通信

这是一个命名管道的实现实例,实现两个进程聊天功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>

int main()
{
char *file = "./test.fifo";
umask(0);
if (mkfifo(file, 0664) < 0) {
if (errno == EEXIST) {
printf("fifo exist!!\n");
}else {
perror("mkfifo");
return -1;
}
}
int fd = open(file, O_RDONLY);
if (fd < 0) {
perror("open error");
return -1;
}
printf("open fifo success!!\n");
while(1) {
char buff[1024];
memset(buff, 0x00, 1024);
int ret = read(fd, buff, 1024);
if (ret > 0) {
printf("peer say:%s\n", buff);
}
}
close(fd);
return 0;
}

管道符实现命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*  这是一个实现管道符的demo
* 命令:ps -ef |grep ssh
* 一个进程运行ps程序,一个进程运行grep程序
* ps程序就需要将结果通过匿名管道传递给grep程序
*/

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
int pid = -1;
int pipefd[2] = {0};

if (pipe(pipefd) < 0 ) {
perror("pipe error");
return -1;
}
pid = fork();
if (pid < 0) {
perror("fork error");
return -1;
}else if (pid == 0) {
//子进程运行grep程序处理ps的结果(从管道读数据)
dup2(pipefd[0], 0);
close(pipefd[1]);
execl("/bin/grep", "grep", "ssh", NULL);
}else {
//父进程运行ps程序,将结果写入管道
dup2(pipefd[1], 1);
close(pipefd[0]);
execl("/bin/ps", "ps", "-ef", NULL);
}
return 0;
}

内存共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*  这是一块共享内存的demo,共享数据
*/

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>

#define IPC_KEY 0x01234567
int main()
{
int shmid = -1;
//int shmget(key_t key, size_t size, int shmflg);
// key:共享内存在系统中的标识啊
// ftok这个接口可以通过一个文件计算出一个key值
// size:共享内存大小
// shmflg:IPC_CREAT 创建|权限
// 返回值:共享内存的操作句柄
shmid = shmget(IPC_KEY, 32, IPC_CREAT|0664);
if (shmid < 0) {
printf("shmget error\n");
return -1;
}
//创建的这个共享内存无法直接操作,因为我们只能操作虚拟地址空间
//中的地址,因此第二步就是将共享内存映射到虚拟地址空间,让我们
//能够通过虚拟地址来访问这块内存
//void *shmat(int shmid, const void *shmaddr, int shmflg);
// shmid: 共享内存句柄
// shmaddr:映射首地址(通常置空)
// shmflg:
// SHM_RDONLY 只读 否则可读可写
// 返回:映射到虚拟地址空间的首地址 失败:(void*)-1
void *shm_start = shmat(shmid, NULL, 0);
if (shm_start == (void*)-1) {
perror("shmat error");
return -1;
}
/*
int i = 0;
while(1) {
sprintf(shm_start, "%s---%d\n", "吃什么呢??", i++);
sleep(1);
}*/
//int shmdt(const void *shmaddr);
// 解除共享内存在虚拟地址空间中的映射关系
// shmaddr: 映射的首地址
// 返回值 成功:0 失败:-1
shmdt(shm_start);
//int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// cmd:
// IPC_RMID 删除共享内存
// buf:存放获取到的共享内存信息
// 删除共享内存(并不会立即删除,会判断映射链接数是否为0)
shmctl(shmid, IPC_RMID, NULL);
return 0;
}

信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*  这是一个演示signal接口修改信号处理方式的demo
* 信号的处理方式有三种:
* 忽略
* 默认
* 自定义
*/

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void sigcb(int signo)
{
static int a = 1;
static int b = 1;
int c = 0;

a++;
sleep(10);
b++;
c = a + b;
printf("recv signo:%d--%d\n", signo, c);
return;
}
int main()
{
//sighandler_t signal(int signum, sighandler_t handler);
// signum: 信号的编号
// handler: 处理方式
// SIG_IGN 忽略
// SIG_DFL 默认
int i = 0;
signal(SIGINT, sigcb);
signal(SIGQUIT, sigcb);
while(1) {
/*
printf("-----------\n");
kill(getpid(), SIGINT);
if (++i == 3) {
signal(SIGINT, SIG_DFL);
}
*/
sigcb(SIGQUIT);
}
return 0;
}

僵尸进程的避免

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*僵尸进程的避免:
* 僵尸进程是子进程先于父进程退出,操作系统会通知父进程说你的子进
* 程挂了,你去收尸吧,但是父进程没有管,所以子进程死不瞑目。
*
* 操作系统如何通知父进程说子进程退出呢?
* 信号:SIGCHLD -17号信号
* 以前因为没有学信号,因此我们避免产生僵尸进程,只能让父进程一直
* 等待子进程的退出(因为实在是不知道子进程到底什么时候退出,),
* 浪费了父进程资源,
*
* 现在学了信号完全可以这样做:
* 自定义信号:SIGCHLD的处理方式,相当于提前告诉进程,当接收到
* 这个信号的时候使用waitpid,这样就不用一直等了;
*/

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

void sigcb(int signo)
{
//使用非阻塞的循环来处理SIGCHLD信号
//因为SIGCHLD信号不是可靠信号,有可能丢失
//因此就有可能漏掉僵尸子进程没有处理,
//所以一旦接收到信号就处理到不能处理为止
while(waitpid(-1, NULL, WNOHANG) > 0);
printf("have child exit!!\n");
}
int main()
{
signal(SIGCHLD, sigcb);
int pid = fork();
if (pid == 0) {
exit(0);
}
while(1) {
printf("------\n");
sleep(1);
}
return 0;
}

本文标题:Linux编程之进程间通信

文章作者:LiuXiaoKun

发布时间:2018年12月08日 - 22:12

最后更新:2018年12月08日 - 22:12

原始链接:https://LiuZiQiao.github.io/2018/12/08/Linux编程之进程间通信/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%