嵌入式Hacker (es-hacker)

Embedded bsp developer enjoys thinking and hacking opensource and develop boards(NanoPi, LicheePi, RPi...)

0%

Linux系统编程-管道入门1

早,继续记录我的学习心得。

当你厌倦了自己的目标时,怎样继续保持专注?

误区:
成功人士说的都是自己如何“满怀热情”去努力实现他们的目标。不管是在商业、体育还是艺术界,我们听到的都是“一切都归结于激情”或者“你必须真的渴望得到它”之类的说法。这让我们认为成功人士会有无限的激情,如果我们感到自己激情消退了,仿佛说明了我们不如他们,这让人很沮丧。

事实是:
他们没把话说完整。成功人士也会和普通人一样感到激情消退,这是任何有都无法逃脱的。熟能生巧。但是你练习的次数越多,它就变得越无聊,越像是机械地重复。

成功的最大威胁不是失败,而是倦怠。我们厌倦了好习惯,因为它们不再让我们开心。就好像写作一样,长期来看会给我带来好的结果,但是总会有那么几天我也会感觉写作是枯燥的。

成功人士更强大的地方在于:
尽管感到枯燥乏味,他们仍想办法坚持下去。

对抗枯燥感的小技巧:
在开始做自己不不想做的事情时: 先一边听音乐一边干,15分钟后再摘下耳机。必须控制时间,因为听得越多,音乐给你带来的满足感就会越少。

以下是正文,先明确一下正文的目的:

  • 举例说明 Linux 下父子进程如何使用管道进行通讯。

目录:

1
一、概述
2
    1. IPC 工具分类
3
    2. 什么是管道
4
    3. 管道的 2 个局限性
5
    4. 管道是最常用的 IPC
6
    5. 使用 pipe() 创建管道
7
二、一个简单的例子 (simple_pipe.c)
8
    1. 举例说明
9
    2. 相关要点
10
三、参考书籍
11
四、欢迎关注我的微信公众号

一、概述

1. IPC 工具分类

  • 通信类 (communication):这类工具关注进程之间的数据交换。

  • 同步类 (synchronization):这类工具关注进程和线程操作之间的同步。

  • 信号类 (signal):尽管信号的主要作用并不在此,但在特定场景下仍然可以将它作为一种同步技术。更罕见的是信号还可以作为一种通信技术:信号编号本身是一种形式的信息,并且可以在实时信号上绑定数据。

2. 什么是管道?

当从一个进程连接数据流到另一个进程时,我们使用术语管道 (pipe),也叫无名管道 (unnamed pipe)。通常是把一个进程的输出通过管道连接到另一个进程的输入。

管道是 UNIX 系统最古老的 IPC ,所有 UNIX 系统都提供这种通信机制。

3. 管道的 2 个局限性

  • 历史上,它是半双工的 (即数据只能在一个方向上流动)。现在,某些系统提供全双工管道,但是为了可移植性,我们不应预先假定系统支持全双工管道。

  • 管道只能在具有公共祖先的两个进程之间使用。通常,一个管道由一个进程创建,在进程调用 fork 之后,这个管道就能在父进程和子进程之间使用了。

4. 尽管有这 2 个局限性,半双工管道仍是最常用的 IPC

每当在管道中键入一个命令序列,让 shell 执行时,shell 都会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出与后一条命令的标准输入相连接。shell 负责安排两个命令的标准输入和标准输出:

  • cmd1 的标准输入来自终端键盘;
  • cmd1 的标准输出传递给 cmd2,作为它的标准输入;
  • cmd2 的标准输出连接到终端屏幕;

shell 所做的工作实际上是对标准输入和标准输出流进行了重新连接,使数据流从键盘输入通过两个命令最终输出到屏幕上。

以 “$ ls | wc -l” 为例:

5. 使用 pipe() 创建管道

1
$ man 2 pipe
2
NAME
3
       pipe, pipe2 - create pipe
4
SYNOPSIS
5
    #include <unistd.h>
6
    int pipe(int filedes[2]);

成功的 pipe() 调用会在数组 filedes 中返回两个打开的文件描述符:一个表示管道的读取端 (filedes[0]),另一个表示管道的写入端 (filedes[1])。

与所有文件描述符一样,可以使用 read() 和 write() 在管道上执行 I/O 操作。一旦向管道的写入端写入数据之后立即就能从管道的读取端读取数据。

二、一个简单的例子 (simple_pipe.c)

1. 举例说明

下面的程序演示了如何将管道用于父进程和子进程之间的通信:

1
int main(int argc, char *argv[])
2
{
3
    int pfd[2];
4
    char buf[BUF_SIZE];
5
    ssize_t numRead;
6
7
    if (argc != 2 || strcmp(argv[1], "--help") == 0)
8
        usageErr("%s string\n", argv[0]);
9
10
    if (pipe(pfd) == -1)    /* Create the pipe */
11
        errExit("pipe");
12
13
    switch (fork()) {
14
    case -1:
15
        errExit("fork");
16
17
    case 0:             /* Child  - reads from pipe */
18
        if (close(pfd[1]) == -1)            /* Write end is unused */
19
            errExit("close - child");
20
21
        for (;;) {              /* Read data from pipe, echo on stdout */
22
            numRead = read(pfd[0], buf, BUF_SIZE);
23
            if (numRead == -1)
24
                errExit("read");
25
            if (numRead == 0)
26
                break;                      /* End-of-file */
27
            if (write(STDOUT_FILENO, buf, numRead) != numRead)
28
                fatal("child - partial/failed write");
29
        }
30
31
        // cleanup
32
        ...
33
    default:            /* Parent - writes to pipe */
34
        if (close(pfd[0]) == -1)            /* Read end is unused */
35
            errExit("close - parent");
36
37
        if (write(pfd[1], argv[1], strlen(argv[1])) != strlen(argv[1]))
38
            fatal("parent - partial/failed write");
39
40
        if (close(pfd[1]) == -1)            /* Child will see EOF */
41
            errExit("close");
42
        wait(NULL);                         /* Wait for child to finish */
43
        exit(EXIT_SUCCESS);
44
    }
45
}

运行效果:

1
$ ./simple_pipe 'msg from parent'
2
msg from parent

单个进程中的管道几乎没有任何用处。一般来讲都是使用管道让两个进程进行通信。为了让两个进程通过管道进行连接,在调用完 pipe() 之后可以调用 fork(),子进程会继承父进程的文件描述符的副本:

虽然父进程和子进程都可以从管道中读取和写入数据,但一般不这么使用管道。通常,在 fork() 之后,其中一个进程应该立即关闭管道的写入端的描述符,另一个则应该关闭读取端的描述符。在上面的例子中,父进程向子进程传输数据,所以它关闭了管道的读取端的描述符 filedes[0],而子进程则关闭管道的写入端的描述符 filedes[1]:

2. 相关要点

  1. 父子进程从同一个管道中读取和写入数据这种做法不好的原因是:

    • 如果两个进程同时试图从管道中读取数据,那么就无法确定哪个进程会首先读取成功——两个进程竞争数据了,即会产生竞争。

    • 使用管道进行双向通信更加简单的方法是:创建两个管道,在两个进程之间发送数据的两个方向上各使用一个。

  2. 管道可以用于任意两个(或更多)相关进程之间的通信,只要在创建子进程的系列 fork() 调用之前通过一个共同的祖先进程创建管道即可。

  3. 关闭未使用管道文件描述符对于正确使用管道非常重要的:

    • 当 read 一个写入端 (write end) 已被关闭的管道时,在所有数据都被读取后,read 会返回 0,这时读取进程就知道文件已结束了。

    • 如果读取进程没有关闭管道的写入端,那么在其他进程关闭了写入描述符之后,读取进程也不会看到文件结束,即使它读完了管道中的所有数据。

    • 当 write 一个读取端已被关闭的管道,内核会向写入进程发送一个SIGPIPE 信号。在默认情况下,这个信号会杀死进程。但进程可以捕获或忽略该信号,这时管道上的 write() 操作因 EPIPE 错误而失败。收到SIGPIPE 信号 和获得 EPIPE 错误对于可以起到标示管道状态的作用。

  4. 在写管道时,常量 PIPE_BUF 规定了内核的管道缓冲区大小。

    • 如果对管道调用 write(),而且要求写的字节数小于等于 PIPE_BUF,则此操作不会与其他进程对同一管道的 write 操作交叉进行。

    • 如果有多个进程同时写一个管道,且要求写入的字节数超过 PIPE_BUF,则所写的数据可能会与其他进程所写的数据交错在一起。

三、参考书籍

  • 《UNIX 环境高级编程 - 15.2 管道》
  • 《Linux / UNIX 系统编程手册 - 44.管道和FIFO》
  • 《Linux 程序设计 - 13.进程间通信:管道》
  • 《UNIX-Linux编程实践教程 - 10.I/O 重定向和管道》

更多值得学习的知识点:

  • 将管道作为一种进程同步的方法;
  • 使用管道连接过滤器;
  • 通过管道与 shell 命令进行通信:popen();
  • 管道和 stdio 缓冲;
  • 命名管道:FIFO;

鉴于大多数人的注意力无法在一篇文章里上集中太久,更多的内容请大家先自行去阅读吧,不是自己理解到的东西是消化不了的。有机会的话我会把更多的读书心得放在后面的文章。

四、欢迎关注我的微信公众号

你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。如果你也对 嵌入式系统和开源软件 感兴趣,并且想和更多人互相交流学习的话,请关注我的公众号:嵌入式Hacker,一起来学习吧,无论是 关注或转发 , 还是赏赐,都是对作者莫大的支持,感谢 各位的大拇指「在看」 ,祝工作顺利,家庭和睦~

这是一篇有趣的文章吗?

欢迎关注我的其它发布渠道