进程间的通信

进程间关系

进程都有父进程,父进程也有父进程,这就形成了一个以init进程为根的家族树。除此以外,进程还有其他层次关系:进程、进程组和会话。
进程组和会话在进程之间形成了两级的层次:进程组是一组相关进程的集合,会话是一组相关进程组的集合。
这样说来,一个进程会有如下ID:

  • ·PID:进程的唯一标识。对于多线程的进程而言,所有线程调用getpid函数会返回相同的值。
  • ·PGID:进程组ID。每个进程都会有进程组ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组ID。
  • ·SID:会话ID。每个进程也都有会话ID。默认情况下,新创建的进程会继承父进程的会话ID。

进程,可以调用以下函数获取进程组ID跟会话ID.

pid_t getpgrp(void);
pid_t getsid(pid_t pid); 

前面提到过,新进程默认继承父进程的进程组ID和会话ID,如果都是默认情况的话,那么追根溯源可知,所有的进程应该有共同的进程组ID和会话ID。但是调用ps axjf可以看到,实际情况并非如此,系统中存在很多不同的会话,每个会话下也有不同的进程组。
为何会如此呢?
就像家族企业一样,如果从创业之初,所有家族成员都墨守成规,循规蹈矩,默认情况下,就只会有一个公司、一个部门。但是也有些“叛逆”的子弟,愿意为家族公司开疆拓土,愿意成立新的部门。这些新的部门就是新创建的进程组。如果有子弟“离经叛道”,甚至不愿意呆在家族公司里,他别开天地,另创了一个公司,那这个新公司就是新创建的会话组。由此可见,系统必须要有改变和设置进程组ID和会话ID的函数接口,否则,系统中只会存在一个会话、一个进程组。
进程组和会话是为了支持shell作业控制而引入的概念。
当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。
在登录shell时,用户可能会使用管道,让多个进程互相配合完成一项工作,这一组进程属于同一个进程组。

当用户通过SSH客户端工具(putty、xshell等)连入Linux时,与上述登录的情景是类似的。

1.进程组


Linux下每个进程(PID)都隶属于一个进程组(PGID)。每个进程组都有一个组长进程,它的PGID和PID相同。进程组是一直存在的,除非进程组里面的进程都退出了,或者加入到其他进程组中。一个进程能够设置自己或者其子进程的PGID,另外,当子进程调用了exec系列函数之后,父进程也不能设置子进程的PGID。

进程获取进程组ID的接口如下:

pid_t getpgid(pid_t pid); 
这是获取进程号为pid的进程所属的进程组号
若参数pid为0就是获取当前进程所属的进程组号

pid_t getpgrp(void);
这函数的作用是获取当前进程的集成组号
相当于 getpgid(0)

修改进程组ID的接口如下:

int setpgid(pid_t pid, pid_t pgid);

如果pid等于pgid,那么pid就成为了其所属的进程组的组长进程,
如果pid等于0,则标识把当前进程设置为pgid的组长进程
如果pgid等于0,则使用pid作为目标pgid

这个函数的含义是,找到进程ID为pid的进程,将其进程组ID修改为pgid,如果pid的值为0,则表示要修改调用进程的进程组ID。该接口一般用来创建一个新的进程组。
下面三个接口含义一致,都是创立新的进程组,并且指定的进程会成为进程组的首进程。如果参数pid和pgid的值不匹配,那么setpgid函数会将一个进程从原来所属的进程组迁移到pgid对应的进程组。

setpgid(0,0)
setpgid(getpid(),0)
setpgid(getpid(),getpid()) 

setpgid函数有很多限制:
·pid参数必须指定为调用setpgid函数的进程或其子进程,不能随意修改不相关进程的进程组ID,如果违反这条规则,则返回-1,并置errno为ESRCH。
·pid参数可以指定调用进程的子进程,但是子进程如果已经执行了exec函数,则不能修改子进程的进程组ID。如果违反这条规则,则返回-1,并置errno为EACCESS。
·在进程组间移动,调用进程,pid指定的进程及目标进程组必须在同一个会话之内。这个比较好理解,不加入公司(会话),就无法加入公司下属的部门(进程组),否则就是部门要造反的节奏。如果违反这条规则,则返回-1,并置errno为EPERM。
·pid指定的进程,不能是会话首进程。如果违反这条规则,则返回-1,并置errno为EPERM。
有了创建进程组的接口,新创建的进程组就不必继承父进程的进程组ID了。最常见的创建进程组的场景就是在shell中执行管道命令,代码如下:cmd1 | cmd2 | cmd3

下面用一个最简单的命令来说明,其进程之间的关系如图4-2所示。

ps ax|grep nfsd 

ps进程和grep进程都是bash创建的子进程,两者通过管道协同完成一项工作,它们隶属于同一个进程组,其中ps进程是进程组的组长。
进程组的概念并不难理解,可以将人与人之间的关系做类比。一起工作的同事,自然比毫不相干的路人更加亲近。shell中协同工作的进程属于同一个进程组,就如同协同工作的人属于同一个部门一样。
引入了进程组的概念,可以更方便地管理这一组进程了。比如这项工作放弃了,不必向每个进程一一发送信号,可以直接将信号发送给进程组,进程组内的所有进程都会收到该信号。
前文曾提到过,子进程一旦执行exec,父进程就无法调用setpgid函数来设置子进程的进程组ID了,这条规则会影响shell的作业控制。出于保险的考虑,一般父进程在调用fork创建子进程后,会调用setpgid函数设置子进程的进程组ID,同时子进程也要调用setpgid函数来设置自身的进程组ID。这两次调用有一次是多余的,但是这样做能够保证无论是父进程先执行,还是子进程先执行,子进程一定已经进入了指定的进程组中。由于fork之后,父子进程的执行顺序是不确定的,因此如果不这样做,就会造成在一定的时间窗口内,无法确定子进程是否进入了相应的进程组。
用户在shell中可以同时执行多个命令。对于耗时很久的命令(如编译大型工程),用户不必傻傻等待命令运行完毕才执行下一个命令。用户在执行命令时,可以在命令的结尾添加“&”符号,表示将命令放入后台执行。这样该命令对应的进程组即为后台进程组。在任意时刻,可能同时存在多个后台进程组,但是不管什么时候都只能有一个前台进程组。只有在前台进程组中进程才能在控制终端读取输入。当用户在终端输入信号生成终端字符(如ctrl+c、ctrl+z、ctr+\等)时,对应的信号只会发送给前台进程组。

shell中可以存在多个进程组,无论是前台进程组还是后台进程组,它们或多或少存在一定的联系,为了更好地控制这些进程组(或者称为作业),系统引入了会话的概念。会话的意义在于将很多的工作囊括在一个终端,选取其中一个作为前台来直接接收终端的输入及信号,其他的工作则放在后台执行。


2.会话

会话是一个或多个进程组的集合,以用户登录系统为例,可能存在如图4-3所示的情况。

 

会话是一个或者多个进程组的集合。
一个会话有一个控制终端建立与控制终端相连接的会话首进程叫做控制进程。一个会话当中分为一个前台进程组和多个后台进程组内核通常发信号给前台进程组的所有进程。
会话的意义在于将多个工作囊括在一个终端,并且取其中的一个工作作为前台,来直接接受该终端的输入输出以及终端信号。其他的工作在后台运行。
建立新会话,可以新打开一个终端也可以使用函数setsid来创建一个新的会话。
 

  系统提供getsid函数来获取进程所属会话,其接口定义如下:

pid_t getsid(pid_t pid); 

 系统提供setsid函数来创建会话,其接口定义如下:

pid_t setsid(void); 

如果这个函数的调用进程不是进程组组长,那么调用该函数会发生以下事情:
1)创建一个新会话,会话ID等于进程ID,调用进程成为会话的首进程。
2)创建一个进程组,进程组ID等于进程ID,调用进程成为进程组的组长。
3)该进程没有控制终端,如果调用setsid前,该进程有控制终端,这种联系就会断掉。


调用setsid函数的进程不能是进程组的组长,否则调用会失败,返回-1,并置errno为EPERM。

这个限制是比较合理的。如果允许进程组组长迁移到新的会话,而进程组的其他成员仍然在老的会话中,那么,就会出现同一个进程组的进程分属不同的会话之中的情况,这就破坏了进程组和会话的严格的层次关系了。


作业控制


Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。


作业和进程组的区别:

在作业内部创建了子进程,该子进程属于进程组而不属于作业;
作业控制:一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,Shell可以运行一个前台作业和任意多个后台作业。
作业类似进程存在进程号一样,作业也存在作业号:

几个简单的命令:
1).& :在运行一个进程后面加上取地址,则说明让该进程到后台运行;例如:./a.out &;
2).jobs:查看所有的后台作业;
3).fg +作业号:将指定作业放置前台;
4).ctrl+z:将前台作业暂停;
6).ctrl+c:作业终止,进程发信号给所有的前台进程;
5).bg +作业号:将之前暂停的转到后台的作业运行起来;
 


终端


终端,是一种仿真器,是一种模拟器
1).控制终端:用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端 。控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端;默认情况 下,每个进程的标准输入、标准输出和标准错误输出都指向控制终端。
2).ttyname()的使用:
char *ttyname(int fd);根据文件描述符来获取对应的文件名称;
查看终端对应的设备:

查看

mytty

每个进程都可以通过一个特殊的设备文件/dev/tty(终端设备文件)访问它的控制终端,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访问。
在上述的例子中却是/dev/pts,那仫/dev/pts和/dev/tty有什仫区别呢?
总的来说可以总结为下面一句话:在界面模式下是/dev/pts,但是在黑屏模式下却是/dev/tty。
3).我们可以通过重定向的方式向另一个终端打印消息。
mytty

4).线路规程:相当于一个过滤器,如果是普通字符则直接忽略;如果是特殊的组合键则将其解释为信号;内核中处理终端设备的模块包括硬件驱动程序和线路规程。 

线路规程

在终端设备中既有输入队列也有输出队列,所以当存在这样一种情况:当你的输入在显示器上无序的时候也仅仅是回显到输出队列时被冲乱,但是在你的输入队列中依然是有序的。
接下来看一看什仫是终端登录过程:
5).所谓的终端登录过程指的是用户在输入用户名和密码验证的过程,下面是我理解的一张终端登录的过程图:
终端登录过程

从上图可以看出终端登录过程可以是如下几个步骤:
1).系统启动时,init进程(也就是1号进程)根据配置文件/etc/inittab确定需要打开哪些终端;
2).getty根据命令行参数打开终端设备作为它的控制终端,把文件描述符0、1、2都指向控制终 端,然后提示用户输入帐号。用户输入帐号之后,getty的任务就完成了,它再执行login程序;
execle(“/bin/login”, “login”, “-p”, username, NULL, envp);
3).如果密码不正确,login进程终止,init会重新fork/exec一个getty进程;如果密码正确,login程序设置一些环境变量,设置当前工作目录为该用户的主目录,然后执行Shell;
execl(“/bin/bash”, “-bash”, NULL);
 

会话和终端的关系

  • 一个会话可以有一个控制终端(controlling terminal)。
  • 建立与控制终端连接的会话首进程被称为控制进程(controlling process)。
  • 一个会话中的几个进程组可被分成一个前台进程组(forkground process group)和几个后台进程组(background process group)。
  • 如果一个会话有一个控制终端,则它有一个前台进程组。
  • 无论何时键入终端的中断键(DELETE或Ctrl+C),就会将中断信号发送给前台进程组的所有进程。
  • 无论何时键入终端的退出键(Ctrl+),就会将退出信号发送给前台进程组的所有进程。
  • 如果终端检测到调制解调器(或网络)已经断开连接,则将挂断信号发送给控制进程(会话首进程)。
     


一.进程间通信(IPC)简介

    由于每个进程都有自己独立的运行环境,因此进程与进程间是相对封闭的。如何让两个封闭的进程之间实现数据通信是进程编程的重点与难点。

Linux的内的进程通信机制基本来源于Unix的系统对Unix的发展做出巨大贡献的两大主力--AT&T公司的贝尔实验室和加州大学伯克利分校 - 。在进程通信领域研究的侧重点不同。

    贝尔实验室对Unix系统早期的进程间通信手段进行了改进与扩充,形成了System V IPC(进程间通信)。互相通信的进程被限定在单个计算机内。而伯克利分校则跳出了解System V IPC的限制,发展出了以套接字(socket)为基本点的进程间通信机制。

Linux的系统将二者的优势全部继承下来现在的Linux系统内比较常用的进程间通信方式有以下几种:
- 传统UNIX系统内的进程通信方式:

        1)无名管道(管)和有名管道(FIFO):

        管道提供了进程间通信消息传递的实体,其原型来自于数据结构的“队列”。无名管道用于具有亲缘关系的进程(例如父子进程,兄弟进程),而有名管道则允许不具有亲缘关系的进程使用。

        2)信号(信号)

        信号是在软件层面上对中断的一种模拟机制,用于通知进程某个事件发生。

    -System V IPC进程通信方式:

        3)消息队列(消息队列)

        消息队列是消息所构成的链表,包括POSIX消息队列与系统V消息队列两种。消息队列克服了管道与信号两种通信方式中信息量有限的缺点。

        4)共享内存(共享内存):

        最有效的进程通信方式。它使得多个进程共享一块内存空间,不同进程间可以实时观察到其他进程的数据更新。不过使用该方式需要某种同步与互斥机制。

        5)信号量(信号量):

        主要作为进程间以及同一进程的不同线程间的同步与互斥手段。

    -BSD进程通信方式:

        6)套接字(socket):

        更广泛的进程通信机制,常用于网络的不同主机之间的进程通信。
        套接字在网络部分再学

二、管道通信

管道是Linux中进程间通信的一种常用方式,它将一个程序的输出直接作为另一个程序的输入。Linux内的管道通信主要有无名管道与有名管道两种。管道本质是内核中的一块缓冲区

/***************************************************************************************/

Linux的管道通信是基于内核的,内核会确保管道通信的同步和互斥。具体来说,Linux的管道通信是一种基于缓冲区的通信方式,内核维护着两个缓冲区,一个是发送端缓冲区,一个是接收端缓冲区,在缓冲区内部实现了同步和互斥。

同步:当发送端往管道中写入数据时,数据首先被写入到发送端缓冲区中,当缓冲区满时,发送端会被阻塞,直到接收端从接收缓冲区中读出数据,此时发送端才能继续写入数据。

互斥:管道通信是一种半双工通信方式。在管道通信中,进程需要通过读取和写入管道来进行通信,但是一个进程只能在一个时刻进行读取或写入操作,因此管道通信是一种半双工通信方式。具体来说,如果一个进程在管道中写入数据,那么这个进程在写入期间是不能读取管道中的数据的,只有写入完成后才能进行读取操作。同理,如果一个进程在管道中读取数据,那么这个进程在读取期间是不能写入数据到管道中的,只有读取完成后才能进行写入操作。发送端和接收端之间是互相独立的,内核会确保同一时间只有一个进程能够读取或写入管道,以保证数据的正确性。

因此,在Linux中,管道通信的同步和互斥是由内核来保证的,应用程序只需要正确地使用管道接口函数,即可实现同步和互斥的管道通信。

/***************************************************************************************/

之前在文件IO中学习open函数的时候我们知道:

普通文件:默认是非阻塞的!!!!

设备文件:默认是阻塞的(比如管道文件)!!!

无名管道和有名管道总结(这里统称为管道):


匿名管道只能用于具有亲缘关系的进程间通信,命名管道用于任意的进程间通信
管道的生命周期随进程
管道提供流服务—字节流传输:数据放在缓冲区中(有序、连接、可靠,传输比较灵活)
优点:传输灵活
缺点:数据粘连(数据在缓冲区中堆积在一起了)
管道自带同步与互斥功能(读写操作和数据大小不超过PIPE_BUF大小,读写操作受保护)
互斥:对临界(公共)资源同一时间的唯一访问性(我操作时别人不能操作),对管道进行数据操作的大小不超过PIPE_BUF=4096的时候,则保证操作的原子性。
同步:对临界资源的时序可控性(我操作完了别人才能操作),避免一个人一直在操作,其他人操作不了
“互斥保证安全,同步保证合理”

管道的生命周期随进程
字节流传输就好像水流一样,而数据报传输就好像冰块一样,如果冰块太她了,就会出现传送不了的情况。
 

多个进程往一个管道写,一个进程读可不可以 为什么?

无名管道(管道)

1、无名管道简介

无名管道是Unix系统内一种原始的进程通信方法。使用无名管道需要注意:

    1.只能用于具有亲缘关系的进程间通信(父子进程、兄弟进程)

    2.半双工通信模式,即无法同时读写管道。管道具有固定的读端与写端

    3.管道可以看做特殊的文件,可以使用read()/write()函数对管道进行读写操作(但是不能使用lseek()进行定位操作)。不过管道不属于文件系统,并且只存放在内存中
 

 

站在文件描述符角度-深度理解管道

 站在内核角度-管道的本质

无名管道读写规则

1)如果读端写端都开启着

  • 管道中若没有数据,则read会阻塞。
  • 若管道中数据满了,则write会阻塞。

        (这是因为此时无名管道读端和写端都开启着 会认为将继续读写 即使管道中没数据或者数据满了 使用read或者write也只会堵塞等待 而不是返回)


2)

  • 若管道所有写端被关闭,则read读完数据后返回0(而不是阻塞),read返回0,表示管道没人写了(即写端全部关闭)没必要再继续读,进程退出。
  • 若管道所有读端被关闭,则write写数据会触发异常返回SIGPIPE信号报错(导致进程退出)

总结:

① 读管道:    1. 管道中有数据,read返回实际读到的字节数。
                        2. 管道中无数据:
                  (1) 管道写端被全部关闭,read返回0 (读到文件结尾)。这个也是我们判断   对方断开连接的方式。
                  (2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
② 写管道:    1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
               2. 管道读端没有全部关闭: 
                  (1) 管道已满,write阻塞。
                  (2) 管道未满,write将数据写入,并返回实际写入的字节数。

个人理解 :要尽量保证管道最终不能有数据 所以 当读端关闭 写端write会异常终止

                                                当写端关闭  读端会将管道的数据读完后后返回0

  • 因为管道的读写特性,用户在操作管理的时候最好是没有用到哪一端,则关闭掉。
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。(原子性操作不可被打断)
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
  • 管道自带同步与互斥:
    • 同步:对临界资源访问的时序可控性(时序控制–我操作完了别人才能操作)
    • 互斥:对临界资源的同一时间唯一访问性(保护–我操作的时候别人不能操作)
  • 管道提供字节流服务,传输方式灵活。但是造成了数据粘连(本质原因:数据之间没有边界)

管道特点

  • 本质是内核的块缓冲区—>多个进程通过访问同一块缓冲区实现数据传输通信。
  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
     


2、无名管道编程

无名管道是基于文件描述符的通信方式。当一个管道被创建时,它会创建两个文件描述符fd[0]与fd[1],其中fd[0]固定用于读管道内容fd[1]固定用于写管道内容

一般步骤:

1.创建包含两个文件描述符的数组int fd[ 2 ],其中fd[0]固定用于读管道,fd[1]固定用于写管道

2.使用pipe()创建一个无名管道

3.使用fork()创建子进程 ,这样父子进程就都各有fd[ 0 ] 和fd[ 1 ].  其中fd[0]固定用于读管道内容,                                             fd[1]固定用于写管道内容。

4.进行读写操作(例如:关闭父进程的f[ 0 ] 使用f[ 1 ]执行写操作到管道 ;关闭子进程的f[ 1 ] 使用                                          f[ 0 ]  执行读操作 读取管道的数据

                               通过这样就实现了方向为  父进程 -> 子进程  的通信)

1.定义 

   函数pipe()

    所需头文件:#include<unistd.h>

    函数原型:int pipe(int fd[])

    函数参数:

        fd[ ]    包含两个文件描述符的数组,其中fd[0]固定用于读管道,fd[1]固定用于写管道

    函数返回值:

        成功:0

        失败:-1
 

  创建管道使用pipe()函数,而其余的操作诸如读取管道read()、写入管道write()、关闭管道close()函数与文件IO的函数使用方式相同,这里不再赘述。

那么如何使用无名管道实现父子进程间的通信呢?

由于无名管道具有固定的读端与写端,因此,如果父子进程需要使用无名管道进行通信,可以进行以下操作:
 

父进程->子进程父进程对自己的fd[1]执行写操作,数据流入管道内,然后子进程对自己的fd[0]执行读操作,得到管道内数据

子进程->父进程子进程对自己的fd[1]执行写操作,数据流入管道内,然后父进程对自己的fd[0]执行读操作,得到管道内数据


 注意:无名管道的工作方式是半双工方式,即在一个进程内要么读管道,要么写管道,无法同时进行读写操作。也就是说,在同一时刻内,要么父进程写数据、子进程读数据(父进程->子进程),要么子进程写数据、父进程读数据(子进程->父进程),数据流动方向唯一,不能同时存在两个数据流动方向。在使用时,对于该进程内未使用的文件描述符应当关闭。
 

使用无名管道编程时需要注意以下事项:

    1.无名管道只能用于具有亲缘关系的进程间(通常是父子进程间)

    2.fd[0]固定用于读取管道,fd[1]固定用于写入管道,两个文件描述符不可弄混否则会报错

    3.只有管道存在读端,向管道内写入数据才有意义,否则会返回SIGPIPE信号报错

    4.如果管道使用完毕,关闭所有的文件描述符即可
 

示例:使用无名管道实现父子进程间的通信(子进程->父进程) 

#include<stdio.h>
 
#include<stdlib.h>
 
#include<unistd.h>
 
#include<string.h>
 
#define MAXLEN 100
 
int main()
 
{
 
    int n;
 
    int fd[2];
 
    pid_t pid;
 
    char message[MAXLEN]={0};
 
    if(pipe(fd)<0)//创建一个无名管道
 
    {
 
        perror("cannot create a pipe");
 
        exit(0);
 
    }
 
    if((pid = fork())<0)//创建子进程
 
    {
 
        perror("cannot fork");
 
        exit(0);
 
    }
 
    else if(pid==0)//子进程
 
    {
 
        printf("This is Child Process\n");
 
        close(fd[0]);//关闭该进程内的fd[0](读端),保留fd[1](写端),即接下来对该管道进行写操作
 
        strcpy(message,"Helloworld\n");  //将字符串赋值到字符数组中
 
        write(fd[1],message,strlen(message));    //将数组里的数据写入管道中
 
        close(fd[1]);//管道使用完毕,关闭fd[1]
 
    }
 
    else//父进程
 
    {
 
        printf("This is Parent Process\n");
 
        close(fd[1]);//关闭该进程内的fd[1](写端),保留fd[0](读端),即接下来对该管道进行读操作
 
        sleep(1);//保证子进程先写数据
 
        n = read(fd[0],message,MAXLEN);    //fd[0]从无名管道中读取数据,读入到message数组中,n 为返回的字符个数
 
        printf("Parent read %d characters, Message is:%s",n,message);
 
        close(fd[0]);//管道使用完毕,关闭fd[0]
 
        waitpid(pid,NULL,0);//父进程等待回收子进程
 
    }
 
    return 0;
 
}

/*******************管道的方向与流管道**********************/

    细心的同学可能发现,我们在示例程序中使用管道的时候,关闭了父进程的写端与子进程的读端,相当于强行规定了管道的数据流动方向(子进程->父进程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值