linux学习:进程通信(管道+信号)

目录

管道

无名管道(PIPE)

特征 

例子,通过pipe向父进程发送一段数据

​编辑

有名管道(FIFO)

特征

例子

 注意

信号

特征

api 

例子1

例子2

例子3

例子4

信号相关的内核数据结构


管道

管道分为无名管道和有名管道

管道,那么可以想象他就像一根水管,连接两个进程,一个进 程要给另一个进程数据,就好像将水灌进管道一样,另一方就可以读取出来了,反过来也一 样

无名管道(PIPE)

常用于一对一的亲缘进程间通信的方式

特征 

  •  没有名字,因此无法使用 open( )。
  • 只能用于亲缘进程间(比如父子进程、兄弟进程、祖孙进程……)通信。
  • 半双工工作方式:读写端分开。
  • 写入操作不具有原子性,因此只能用于一对一的简单通信情形。
  • 不能使用 lseek( )来定位。
  • 只能在一个进程中被创建出来,然后通过继承的方式将他的文件描述符传递给子进程
  • 两个文件描述符,一个只能用来读,另一个只能用来写,“半双工”通信方式

例子,通过pipe向父进程发送一段数据

1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <errno.h>
6
7 int main(int argc, char **argv)
8 {
9      int fd[2]; // 1.用来存放 PIPE 的两个文件描述符
10
11     if(pipe(fd) == -1) // 2.创建 PIPE,并将文件描述符放进 fd[2]中
12     {
13     perror("pipe()");
14         exit(1);
15     }
16
17     pid_t x = fork(); // 3.创建一个子进程,他将会继承 PIPE 的描述符
18
19     if(x == 0) // 子进程
20     {
21         char *s = "hello, I am your child\n";
22         write(fd[1], s, strlen(s)); // 4.通过写端 fd[1]将数据写入 PIPE
23     }
24
25     if(x > 0) // 父进程
26     {
27         char buf[30];
28         bzero(buf, 30);
29
30         read(fd[0], buf, 30); // 5.通过读端 fd[0]将数据从 PIPE 中读出
31         printf("from child: %s", buf);
32     }
33
34     close(fd[0]); // 关闭文件描述符
35     close(fd[1]);
36     return 0;
37 }

有名管道(FIFO)

存在于文件系统之中,提供写入原子性特征

特征

  • 有名字,存储于普通文件系统之中。
  • 任何具有相应权限的进程都可以使用 open( )来获取 FIFO 的文件描述符。
  • 跟普通文件一样:使用统一的 read( )/write( )来读写。
  • 跟普通文件不同:不能使用 lseek( )来定位,原因同 PIPE。
  • 具有写入原子性,支持多写者同时进行写操作而数据不会互相践踏。
  • First In First Out,最先被写入 FIFO 的数据,最先被读出来。

例子

示两个普通进程(Jack 和 Rose)如何通过 FIFO 互相传递信息:Jack 从键盘接收一段输入并发送给 Rose,Rose 接收到数据之后将 其显示到屏幕上

.h文件  定义管道名字

1 #ifndef _HEAD4FIFO_H_
2 #define _HEAD4FIFO_H_
3
4 #include <stdio.h>
5 #include <unistd.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include <fcntl.h>
9
10 #include <sys/stat.h>
11 #include <sys/types.h>
12
13 #define FIFO "/tmp/fifo4test" // 有名管道的名字
14
15 #endif

Jack.c文件   写数据

1 #include "head4fifo.h" 2
3 int main(int argc, char **argv)
4 {
5     if(access(FIFO, F_OK))
6     {
7         mkfifo(FIFO, 0644);
8     }
9
10     int fifo = open(FIFO, O_WRONLY); // 以只写方式打开 FIFO
11
12     char msg[20];
13     bzero(msg, 20);
14
15     fgets(msg, 20, stdin);
16     int n = write(fifo, msg, strlen(msg)); // 将数据写入 FIFO
17
18     printf("%d bytes have been sended.\n", n);
19     return 0;
20 }

Rose.c   读数据

1 #include "head4fifo.h" 2
3 int main(int argc, char **argv)
4 {
5     if(access(FIFO, F_OK))
6     {
7         mkfifo(FIFO, 0644);
8     }
9
10     int fifo = open(FIFO, O_RDONLY); // 以只读方式打开管道
11
12     char msg[20];
13     bzero(msg, 20);
14
15     read(fifo, msg, 20); // 将数据从 FIFO 中读出
16     printf("from FIFO: %s", msg);
17
18     return 0;
19 }

 注意

  • 代码第 5 行中的函数 access( )通过指定参数 F_OK 可用来判断一个文件是否存在, 另外还可以通过别的参数来判断文件是否可读、是否可写、是否可执行等
  • 当刚开始运行 Jack 而尚未运行 Rose,或者刚开始运行 Rose 而尚未运行 Jack 时, open 函数会被阻塞,因为管道文件(包括 PIPE、FIFO、SOCKET)不可以在只有读端或 者只有写端的情况下被打开
  • 当 Jack 已经打开但还没写入数据之前,Rose 将在 read( )上阻塞睡眠,直到 Jack 写入数据完毕为止。因为缺省状态下是以阻塞方式读取数据的,可以使用 fcntl( )来使得 fifo 变成非阻塞模式
  • 不仅打开管道会有可能发生阻塞,在对管道进行读写操作时也有可能发生阻塞

可以多个进程写入管道,由管道一一写到日志文件,例如日志文件

信号

唯一一种异步通信方式

信号是一种比较特别的 IPC,大部分的信号是异步的,换句话讲:一般情况下,进程什 么时候会收到信号、收到什么信号是无法事先预料的(除了某几个特殊的信号之外),信号 的到来就像你家门铃的响起一样,你不知道他什么时候会响

特征

前面 31 个信号都有一个特殊的名字,对应 一个特殊的事件,比如 1 号信号 SIGHUP(Signal Hang UP),表示每当系统中的一个 控制终端被关闭(即挂断,hang up)时,即会产生这个信号,有时会将他们称为非实时 信号,这些信号都是从 Unix 系统继承下来的,他们还有个名称叫“不可靠信号”

  • 非实时信号不排队,信号的响应会相互嵌套。
  • 如果目标进程没有及时响应非实时信号,那么随后到达的该信号将会被丢弃。
  • 每一个非实时信号都对应一个系统事件,当这个事件发生时,将产生这个信号。
  • 如果进程的挂起信号中含有实时和非实时信号,那么进程优先响应实时信号并且会 从大到小依此响应,而非实时信号没有固定的次序

 后面的 31 个信号(从 SIGRTMIN[34] 到 SIGRTMAX[64])是 Linux 系统新增的 实时信号,也被称为“可靠信号”

  • 实时信号的响应次序按接收顺序排队,不嵌套。
  • 即使相同的实时信号被同时发送多次,也不会被丢弃,而会依次挨个响应。
  • 实时信号没有特殊的系统事件与之对应

 

 收到信号处理步骤

  1. 如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为 止。否则进入 2
  2. 如果该信号被捕捉,那么进一步判断捕捉的类型
    • 如果设置了响应函数,那么执行该响应函数
    • 如果设置为忽略,那么直接丢弃该信号
    • 否则进入 3
  3. 执行该信号的缺省动作

api 

 

目标进程必须先使用 signal( )来为某个信号设 置一个响应函数,或者设置忽略某个信号,才能改变信号的缺省行为,这个过程称为“信号 的捕捉”。注意,对一个信号的“捕捉”可以重复进行,signal( )函数将会返回前一次设 置的信号响应函数指针。对于所谓的信号响应函数的接口,规定必须是:void (*)(int);

如果一个进程临时不想响应某个或者某些信号,可以通过设置“阻塞掩码 (block mask)”来达到此目的。在设置信号的阻塞掩码时,并不一定要挨个地设置,而 是可以多个信号同时设置,这时就需要用到所谓的信号集

信号所携带的额外的数据是下面这个联合体

union sigval
{
    int sigval_int;
    void * sigval_prt;
}

利用 siqqueue( )发送信号的同时可以携带一个整型数据或者一个 void 型 指针,目标进程要想获取这些额外的数据

act 参数比较复杂,其类型结构体 struct sigaction 的定义如下

struct sigaction
{
    void (*sa_handler)(int);//标准信号响应函数指针
    void (*sa_sigaction)(int, siginfo_t *, void *);//扩展信号响应函数指针
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

例子1

信号的“发送”和“捕捉”——在命令行给一个指定的进程发送某些信 号,观察设置信号响应的三种处理方式

1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <signal.h>
4
5 void f(int sig)
6 {
7     printf("catched a signal: %d\n", sig);
8 }
9
10 int main(int argc, char **argv)
11 {
12     signal(SIGHUP, SIG_IGN); // ① 设置 SIGHUP 响应动作为:忽略
13     signal(SIGINT, SIG_DFL); // ② 设置 SIGINT 响应动作为:缺省
14     signal(SIGQUIT, f); // ③ 设置 SIGQUIT 响应动作为:执行函数 f( )
15
16     printf("[%d]: I am waitting for some signal...\n", 17 getpid());
18     pause();// 暂停进程,静静等待信号的到来…
19
20     return 0;
21 }

例子2

信号的“阻塞”操作——子进程给父进程发送一个信号,父进程 先阻塞该信号,随后解除阻塞的过程

1 #include <stdio.h>
2 #include <signal.h>
3
4 void sighandler(int sig)
5 {
6     printf("[%d]: catch %d.\n", getpid(), sig);
7 }
8
9 int main(int argc, char **argv)
10 {
11     pid_t x = fork();
12
13     if(x > 0) // 父进程
14     {
15         signal(SIGINT, sighandler); // 设置 SIGINT 的响应函数
16
17         sigset_t sigmask;
18         sigemptyset(&sigmask);
19         sigaddset(&sigmask, SIGINT); // 将 SIGINT 添加到信号集中
20
21         #ifdef TEST
22             printf("[%d]: block SIGINT...\n", getpid());
23             sigprocmask(SIG_BLOCK, &sigmask, NULL); // 设置阻塞
24         #endif
25         sleep(5); // 睡眠 5 秒,信号在此期间到来。
26         #ifdef TEST
27             printf("[%d]: unblock SIGINT...\n", getpid());
28             sigprocmask(SIG_UNBLOCK, &sigmask, NULL); // 解除阻塞
29         #endif
30         wait(NULL); // 让子进程先退出,从而正确显示 Shell 命令提示
30     }
31
32     if(x == 0)
33     {
34         sleep(1); // 睡眠 1 秒钟,保证父进程做好准备工作
35         if(kill(getppid(), SIGINT) == 0) // 给父进程发送信号 SIGINT
36         {
37             printf("[%d]: SIGINT has been sended!\n", 38 getpid());
39         }
40     }
41
42     return 0;
43 }

例子3

“实时信号”和“非实时信号”的区别——进程 machine_gun 向 target “开火”:将所有信号(除了 SIGKILL 和 SIGSTOP)“同时”发送给 target,观察进程 怎么处理这些信号。为了体现 target“同时”收到了这些信号,可以让其先对所有代码阻 塞一段时间,等收完全部信号之后,再同时一并放开阻塞

target.c

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <signal.h>
4
5 void sighandler(int sig)
6 {
7     fprintf(stderr, "catch %d.\n", sig);
8 }
9
10 int main(int argc, char **argv)
11 {
12     sigset_t sigs;
13     sigemptyset(&sigs);
14
15     int i;
16     for(i=SIGHUP; i<=SIGRTMAX; i++)
17     {
18         if(i == SIGKILL || i == SIGSTOP)
19             continue;
20
21         signal(i, sighandler); // 为信号 i 设置响应函数
22         sigaddset(&sigs, i); // 将信号 i 添加到信号集中
23     }
24
25     printf("[%d]: blocked signals for a while...\n", getpid());
26     sigprocmask(SIG_BLOCK, &sigs, NULL); // 阻塞所有信号
27     sleep(10);
28
29     printf("[%d]: unblocked signals.\n", getpid());
30     sigprocmask(SIG_UNBLOCK, &sigs, NULL); // 放开所有阻塞
31
32     return 0;
33 }

machine_gun,c

1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <signal.h>
5
6 int main(int argc, char **argv)
7 {
8     if(argc != 2)
9     {
10         printf("Usage: %s <target-PID>\n", argv[0]);
11     }
12     int i;
13     for(i=SIGHUP; i<=SIGRTMAX; i++)
14     {
15         if(i == SIGKILL || i == SIGSTOP || // 不可捕捉的信号不发
16             i == 32 || i == 33) // 未定义的信号不发
17             continue;
18
19         kill(atoi(argv[1]), i); // 向指定进程发送信号 i
20     }
21
22     return 0;
23 }

输出结果中,省略的部分是严格从大到小的实时信号,可见如果一个进程如果同 时收到多个实时信号时,他们的响应次序是按照信号值由大到小排队的。下半部分从 1 到 31 的信号值是无序的,说明非实时信号的响应是不排队的,还注意到 target 没有打印 18 号信号!说明非实时信号是不可靠的,在传递的过程中有可能被丢弃 

例子4

进程间如何使用“扩展信号响应函数”来通信——信号发送者携带额外 的数据,目标进程获取这些数据

1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <strings.h>
4 #include <unistd.h>
5 #include <signal.h>
6
7 void sighandler(int sig, siginfo_t *sinfo, void *p)
8 {
9     printf("catch %d.\n", sig);
10
11     if(sinfo->si_code == SI_QUEUE) //判断信号是否由 sigqueue 发送
12     {
13         printf("%d\n", sinfo->si_int);
14     }
15 }
17 int main(int argc, char **argv)
18 {
19     pid_t x = fork();
20
21     if(x > 0)
22     {
23         struct sigaction act;
24         bzero(&act, sizeof(act));
25         act.sa_sigaction = sighandler;
26         act.sa_flags |= SA_SIGINFO; // 该选项确保使用扩展响应函数
27         sigaction(SIGINT, &act, NULL); // 捕捉 SIGINT
28
29         pause(); // 坐等信号的到来……
30     }
31
32     if(x == 0)
33     {
34         sleep(1);
35
36         union sigval data;
37         data.sival_int = 100; // 额外数据
38         sigqueue(getppid(), SIGINT, data); // 给父进程发 SIGINT
39     }
40
41     return 0;
42 }

信号相关的内核数据结构

  • 每一个线程都使用一个 PCB(即 task_struct)来表示,因此 pending(不是指针) 就是一个线程单独私有的,当我们使用 pthread_kill( )给一个指定的线程发送某信号时, 这些信号将会被存储在这个链队列中
  • signal 是一个指向线程共享的信号挂起队列相关结构体的指针,实际上,一个线程 组(即一个进程)中的所有线程中的signal指针都指向同一个结构体,当我们使用诸如kill( ) 来给一个进程发送某信号的时候,这些信号将会被存储在 shared_pending 这个线程共享 的链队列中
    • 如果一个进程中有超过 1 条线程,那么这些共享的挂起信号将会被随机的某条线程响 应,为了能确保让一个指定的线程响应来自进程之外的、发送给整个进程的某信号,一般的 做法如下
    • 除 了指 定要 响 应某 信号 的线 程 外, 其他 线 程对 这些 信号 设 置阻 塞。 即 使 用 sigprocmask( )或者 pthread_sigmask( )将这些需要阻塞的信号添加到信号阻塞掩码 blocked 当中。
  • sighand 也是一个指针,因此也是进程中的所有线程共享的,他指向跟信号响应函 数相关的数据结构,结构体 struct sighand_struct{}中的数组 action 有 64 个元素,一 一对应 Linux 系统支持的 64 个信号(其中 0 号信号是测试用的,32 号和 33 号信号保留), 每一个元素是一个 sigaction{}结构体,其成员就是标准 C 库函数 sigaction( )中的第二 个参数的成员,可见,该函数相当于是一个应用层给内核设置信号响应策略的窗口

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值