目录
程序结束类型
(1)正常终止: return、 exit、 exit
(2)非正常终止:自己或他人发信号终止进程
atexit注册进程终止处理函数
atexit函数基本用法如下:
atexit可以注册多个进程终止处理函数,先注册的后执行(先进后出,和栈一样):
#include <stdio.h>
#include <stdlib.h>
void catchend1(){
printf("the file end1\n");
}
void catchend2(){
printf("the file end2\n");
}
int main(void){
atexit(catchend1);
atexit(catchend2);
printf("hello\n");
return 0;
}
运行如下:
return、exit和exit的区别: return和exit效果一 样,都是会执行进程终止处理函数,但是用_exit终止进程时并不执行atexit注册的进程终止处理函数。
环境变量
可以使用export来查看环境变量
每一个进程都有一份所有环境变量构成的表格,也就是我们当前的进程可以直接使用这些环境变量。进程环境表其实就是一个字符串数组,一般是用environ变量来指向它。
environ是一个全局变量,程序可以通过environ直接使用环境变量
所以我们构建的程序可以无条件的直接使用系统中的环境变量,即当程序使用环境变量时,程序就和操作系统环境相关。
同样的,我们也可以使用getenv函数来获取指定需要的环境变量。
进程运行的虚拟地址空间
操作系统中每个进程在独立地址空间中运行
每个进程的逻辑地址空间均为4GB (32位系统)
0-1G为OS,1-4G为应用
虚拟地址到物理地址空间的映射意义:进程隔离,提供多进程同时运行
进程
进程定义
进程是程序的一次执行(程序运行的开始到程序终止,例如执行./a.out的开始,到执行完成./a.out)
进程ID
我们可以用ps来查看当前命令行下的进程
ps -aux可以查看所有进程的详细信息
(1)getpid,getppid,getuid.geteuid,getgid.getegid获取进程号
getpid:获取当前进程的id
getuid:获取父进程id
getuid:获取用户d
geteuid:获取有效用户id
getgid:获取组id
getegid:获取有效组id
(2)实际用户ID和有效用户ID有一定区别:
- 实际用户ID:标识登录用户的ID,即发起操作的用户的UserID。
- 有效用户ID:一般和实际用户ID相同,但可以修改,用于决定进程对文件的访问权限。
这章主要介绍getpid,getppid
主要原理就是去当前进程的PCB进程控制块结构体下找相应的pid数值
手册内容如下:
编写程序查看该系统调用效果:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(void){
pid_t pid,ppid = -1;
pid = getpid();
ppid = getppid();
printf("pid:%d\n",pid);
printf("parent id:%d\n",ppid);
return 0;
}
运行结果如下:
该程序执行时,bash为父进程(脚本进程),程序执行结束后进程消失。
在新的进程建立时,已使用过的进程id不会被重复使用,操作系统会让进程号继续向后分配进程id
这里我不断地创建ps进程,为ps分配的进程号不断向后递增,而不是使用之前结束的进程的进程号。
多线程调度原理
操作系统可以同时运行多个进程
宏观上并行(cpu调度速度很快,给人的客观感觉上如同多个程序同时运行)
微观上串行(事实上,进程运行时,一次只能运行一个进程,但是cup的调度速度足够快,使得多个进程可以迅速地交叉串行)
fork(创建子进程)的内部原理
(1)进程的分裂生长模式。如果操作系统需要-一个新进程来运行一个程序,那么操作系统会用一个现有的进程来复制生成一一个新进程。老进程叫父进程,复制生成的新进程叫子进程。
(2) fork的演示
示例代码如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(){
int pid = -1;
pid = fork();
if (pid==0)
{
//子进程
printf("pid:%d\n",getpid());
printf("parent pid is %d\n",getppid());
}
if (pid>0){
//父进程
printf("parent pid:%d\n",getpid());
printf("---------------------\n");
}
return 0;
}
结果如下:
父进程此时的pid为4742,子进程为4743,但是我们在子进程调用的getppid函数返回的却不是真正的父进程pid。
原因是:
fork函数创建子进程是通过复制父进程的基础上进行修改,所以运行./fork文件时,有两个进程在并发地执行,而父进程很有可能在子进程终止前终止,故会将父进程权限上交,变为init进程,导致getppid变化。
(3) fork函数调用一次会返回2次,返回值等于0的就是子进程,而返回值大于0的就是父进程。
(4)典型使用fork的方法:使用fork后用if判断返回值,返回值大于0时就是父进程,等于0时就是子进程。
(5) fork的返回值在子进程中等于0,在父进程中等于本次fork创建的子进程的进程ID。
子进程继承父进程中打开的文件
(1)父进程先open打开一个文件得到fd,然后在fork创建子进程。之后在父子进程中各自write向fd中写入内容。文件得到的内容会是怎样?
编写代码:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd = -1;
fd = open("1.txt",O_RDWR | O_TRUNC);
if (fd==-1){
perror("open failed.\n");
}
pid_t pid = -1;
pid = fork();
if (pid>0){
//父进程
write(fd,"hello",sizeof("hello"));
}else{
write(fd,"world",sizeof("world"));
}
return 0;
}
结果如下:
(2)测试结论是:接续写。实际上本质原因是父子进程之间的fd对应的文件指针是彼此关联的(很像O_APPEND标志)。
(3)实际测试时有时候会看到只有一个字符串,有点像分别写。但是实际不是,原因是因为该测试代码较短,导致父子其中一个进程先终止的情况下关闭了文件,后运行的进程无法打开文件。
父子进程各自独立打开同一文件实现共享
(1)父进程open打开1.txt然后写入,子进程打开1.txt然后写入,结论是:两个进程对文件分别写。
原因是父子进程分离后才各自打开的1. txt,这时候这两个进程的PCB已经独立了,文件表也独立了,因此2次读写是完全独立的。
验证代码;
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
pid_t pid = -1;
pid = fork();
if (pid>0){
//父进程
int fd = -1;
fd = open("1.txt",O_RDWR | O_APPEND);
if (fd==-1){
perror("open failed.\n");
}
write(fd,"hello",sizeof("hello"));
sleep(1);
}else{
//子进程
int fd = -1;
fd = open("1.txt",O_RDWR | O_APPEND);
if (fd==-1){
perror("open failed.\n");
}
write(fd,"world",sizeof("world"));
sleep(1);
}
return 0;
}
运行结果:
(2) open时使用O_APPEND标志看看会如何?通过实践可得,该标准可以把父子进程各自打开的fd文件指针关联起来。
代码在之前的基础上为open添加参数O_APPEND即可。
运行结果:
所以父子进程之间并不是毫无关联,父进程在创建子进程前的行为还是会影响到子进程,但是创建子进程后两者独立运行互不干扰。本质原因就是因为fork内部实际上已经复制父进程的PCB生成了一个新的子进程,并且fork返回时子进程已经完全和父进程脱离并且独立被0S调度执行。子进程最终目的是要独立去运行另外的程序。
进程的诞生
新诞生的进程一般是父进程的子进程,通常使用fork、vfork函数
进程的消亡
进程消亡通常指的是进程的终止,终止有正常终止和异常终止两种。
进程在运行时需要消耗一定的系统资源(内存和I/O).。进程结束时,一般需要回收该进程的系统资源。 一般进程终止后操作系统 os会回收该进程运行时申请的系统资源。但是进程本身也会占用8kb的资源(主要为task_struct和栈外其余内存空间,该资源无法由操作系统os回收,需要父进程为其回收。
进程自身资源回收
前提:子进程优先于父进程结束,子进程除task_struct和栈外其余内存空间皆清理完成。
在前提满足的情况下,父进程可以使用wait或waitpid以显式回收子进程的剩余待回收的内存资源(无法被操作系统os回收的本身资源)并获得子进程退出状态。
父进程也可以不使用wait或waitpid回收子进程,此时父进程完成结束时一样会回收子进程的剩余待回收的内存资源(无法被操作系统os回收的本身资源)。
在子进程终止和被父进程回收子进程剩余资源的这段时间,子进程剩余待回收资源无法回收,我们称此时的子进程为僵尸进程。
孤儿进程
当父进程先于子进程结束,子进程会成为一个孤儿进程。
在linux系统中规定:所有的孤儿进程都会自动成为一个特殊进程(进程1,也就是init进程)的子进程。
父进程wait工作原理
子进程结束时,系统会向父进程发送SIGCHILD信号。父进程在调用wait函数后阻塞(阻塞式),父子进程之间异步,父进程收到信号后从阻塞状态被唤醒回收僵尸子进程。
若父进程没有任何子进程,则wait返回错误(避免了我们在编写操作时,父进程没有子进程,系统不发送信号,导致父进程一直阻塞锁死)。
wait函数:
wait的参数为status,status用来返回子进程结束时的状态,wait函数的返回值为pid_t,这个返回值为本次wait回收的子进程pid。因为当前父进程可能有多个子进程,所以wait函数阻塞直到其中一个子进程结束,wait函数就会返回该子进程的pid,且可以通过传入的status参数得到相关的状态信息。
关于status参数,我们可以用WIFEXITED、WIFSIGNALED、WEXITSTATUS这几个宏来获取子进程的退出状态。
WIFEXITED:判断进程是否正常终止。
WIFSIGNALED:判断进程是因接收信号而非正常终止。
WEXITSTATUS:用来得到正常终止时的进程返回值。
示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t re=-1;
int status=-1; pid_t pid = -1;
re = fork();
if (re==0){
//子进程
printf("child's pid:%d\n",getpid());
return 1;
}
if (re>0){
//父进程
sleep(1);//保证子进程先终止
printf("parent--------------------\n");
pid = wait(&status);
printf("child pid is %d\n",pid);
if (WIFEXITED(status)){
printf("the child terminated normally,and [return number] is %d\n",WEXITSTATUS(status));
}
}
if (re==-1)
{
perror("fork error");
return -1;
}
return 0;
}
结果如下:
使用waitpid实现wait的效果
ret = waitpid(-1, &status, 0) ;
-1表示不等待某个特定PID的子进程而是回收任意一个子进程,0表示用默认的方式(阻塞式)来进行等待,返回值ret是本次回收的子进程的PID,和wait函数功能相同。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t re=-1;
int status=-1; pid_t pid = -1;
re = fork();
if (re==0){
//子进程
printf("child's pid:%d\n",getpid());
return 1;
}
if (re>0){
//父进程
sleep(1);//保证子进程先终止
printf("parent--------------------\n");
pid = waitpid(-1, &status, 0);
printf("child pid is %d\n",pid);
if (WIFEXITED(status)){
printf("the child terminated normally,and [return number] is %d\n",WEXITSTATUS(status));
}
}
if (re==-1)
{
perror("fork error");
return -1;
}
return 0;
}
结果和使用wait无异。
(2) ret = waitpid(pid, &status, 0) ;
等待回收PID为pid的这个子进程,如果当前进程并没有一个ID号为pid的子进程,则返回值为-1;如果成功回收了pid这个子进程则返回值为回收的进程的PID。
示例;
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t re=-1;
int status=-1; pid_t pid = -1;
re = fork();
if (re==0){
//子进程
printf("child's pid:%d\n",getpid());
return 1;
}
if (re>0){
//父进程
sleep(1);//保证子进程先终止
printf("parent--------------------\n");
//pid = waitpid(-1, &status, 0); 阻塞式回收
pid = waitpid(re, &status, 0); //回收指定pid进程
printf("child pid is %d\n",pid);
if (pid==-1){
printf("reclaim failed\n");
}else{
printf("process %d is reclaimed\n",pid);
}
}
if (re==-1)
{
perror("fork error");
return -1;
}
return 0;
}
正常回收成功的结果为:
(3)ret = waitpid(pid, &status,WNOHANG);这种表示父进程要非阻塞式的回收子进程。此时如果父进程执行waitpid时子进程已经先结束等待回收则waitpid直接回收成功,返回值是回收的子进程的PID;如果父进程waitpid时子进程尚未结束则父进程立刻返回(非阻塞)。
子进程先结束,父进程回收成功时:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t re=-1;
int status=-1; pid_t pid = -1;
re = fork();
if (re==0){
//子进程
printf("child's pid:%d\n",getpid());
return 1;
}
if (re>0){
//父进程
sleep(1);//让子进程先终止
printf("parent--------------------\n");
//pid = waitpid(-1, &status, 0); 阻塞式回收
//pid = waitpid(re, &status, 0); //回收指定pid进程
pid = waitpid(re, &status, WNOHANG); //非阻塞式回收进程
printf("child pid is %d\n",pid);
if (pid==0){
printf("no process need reclaim\n");
}else if (pid==-1){
printf("error\n");
}else{
printf("process %d is reclaimed\n",pid);
}
}
if (re==-1)
{
perror("fork error");
return -1;
}
return 0;
}
运行结果为:
此时父进程回收成功。
父进程比子进程先结束,父进程回收不到指定进程时:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t re=-1;
int status=-1; pid_t pid = -1;
re = fork();
if (re==0){
//子进程
printf("child--------------------\n");
printf("child's pid:%d\n",getpid());
return 1;
}
if (re>0){
//父进程
printf("parent--------------------\n");
//pid = waitpid(-1, &status, 0); 阻塞式回收
//pid = waitpid(re, &status, 0); //回收指定pid进程
pid = waitpid(re, &status, WNOHANG); //非阻塞式回收进程
printf("child pid is %d\n",pid);
if (pid==0){
printf("no process need reclaim\n");
}else if (pid==-1){
printf("error\n");
}else{
printf("process %d is reclaimed\n",pid);
}
}
if (re==-1)
{
sleep(1);//让父进程先终止
perror("fork error");
return -1;
}
return 0;
}
运行结果为:
父进程比子进程先此时父进程回收成功。
竞态
(1)竞态全称是:竞争状态,多进程环境下,多个进程同时抢占系统资源(内存、CPU、文件I0)
(2)竞争状态对os来说是很危险的,此时os如果没处理好就会造成结果不确定。
(3)写程序当然不希望程序运行的结果不确定,所以我们写程序时要尽量消灭竞争状态。操作系统为我们提供了一系列的消灭竟态的机制,我们需要做的是在合适的地方使用合适的方法来消灭竟态。
exec族函数执行子进程新程序
exec族函数包含:
fork创建子进程的目的是为了执行新程序(fork创建了子进程后, 子进程和父进程同时被OS调度执行,因此子进程可以单独的执行一个程序,这个程序宏观上将会和父进程同时进行)
当我们想在子进程运行代码时,可以直接在子进程的if中写入想要执行的新程序的代码。这但是这种方式不够灵活,我们必须知道源代码,且源代码长度不可控。
所以我们可以引入exec函数,将需要运行的程序单独编写和编译链接成可执行程序,再调用exec执行相关可执行程序。
execl与execv:
这两个函数是最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。
execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成(l为list的缩写)
int execl(const char *path, const char *arg, ...,NULL);
execv是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。
char *const arg[] = {const char *arg, ... ,NULL}
int execv(const char *path, arg);
execl实例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid=-1;
pid = fork();
if (pid==0){
printf("child-------\n");
execl("/bin/ls","ls","-l","-a",NULL);
return 0;
}
else if(pid>=0){
printf("parent------\n");
}else{
perror("error");
}
return 0;
}
运行结果:
execl实例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid=-1;
pid = fork();
if (pid==0){
printf("child-------\n");
char *const arg[] = {"ls","-l","-a",NULL};
execv("/bin/ls",arg);
return 0;
}
else if(pid>=0){
printf("parent------\n");
}else{
perror("error");
}
return 0;
}
运行结果和上方execl结果一样
execlp和execvp
这两个函数与execl和execv的主要区别是:
execl和execv需要传入完整路径和文件名,根据完整路径找不到即报错。添加p之后的函数会先在PATH路径下寻找文件,若找不到,则会在传入的完整路径下寻找。
execle和execvpe
(1) main函数的原型其实不止是int main(int argc, char **argv), 而可以是:
int main(int argc,char **argv,char * *env)
第三个参数是一个字符串数组,内容为环境变量。
(2)如果用户在执行这个程序时没有传递第三个参数,则程序会自动从父进程继承一份环境变量(默认来源于操作系统os中的环境变量) ;如果我们使用execle或者execvpe时传一个自定义的envp数组, 则该程序中的实际环境变量变为我们当前传递的envp。
进程状态
进程的5种状态:
(1)就绪态。该进程当前所有的运行条件就绪,只要得到了CPU时间就能直接运行。
(2)运行态。就绪态时得到了CPU,进入运行态开始运行。
(3)僵尸态。进程已结束但是父进程未及时回收。
(4)等待态(浅度睡眠&深度睡眠),进程在等待某种条件,条件成熟后可进入就绪态。等待态下就算你给他CPU调度进程也无法执行。浅度睡眠等待时进程可以被(信号)唤醒,而深度睡眠等待时不能被唤醒只能等待的条件到了才能结束睡眠状态。
(5)暂停态。暂停并不是进程的终止,只是被(信号)暂停了,可以回复。
system函数简介
(1)system函数相当于fork+exec联合使用。
(2)system为原子操作。
原子操作:原子操作一旦开始,就不会被打断,直到执行结束。
原子操作的好处就是不会被打断(不会引来竞争状态),坏处是自己可能单独连续占用CPU时间太长影响系统整体实时性,因此应该尽量避免不必要的原子操作,就算不得不使用原子操作,也应该尽量将原子操作的时间缩短。
进程查看命令ps
(1)ps -ajx
偏向于显示各种有关的ID号
(2)ps-aux偏向于显示进程各种占用资源
向进程发送信号指令kill
(1)kill -信号编号进程ID,向一个进程发送一个信号
(2)kill -9 xxx, 将向xxx这个进程发送9号信号,表示结束此进程
何谓守护进程
(1)守护进程:daemon,表示守护进程,简称为d ( 进程名后面带d的基本就是守护进程)。
(2)长期运行(一般是开机运行直到关机时关闭)。
(3)与控制台脱离(普通进程都和运行该进程的控制台相绑定,表现为:如果终端被强制关闭,则此终端中运行的所有进程都被会关闭,背后的问题还在于会话)。
(4)服务器(Server)程序就是一个一直在运行的程序,可以为我们提供某种服务(如nfs服务器为我们提供nfs通信方式),当程序需要这种服务时,我们可以调用服务器程序(和服务器程序通信以得到服务器程序的帮助)来进行这种服务操作。
常见守护进程
(1)syslogd:系统日志守护进程,提供syslog功能。
(2) cron:cron进程用来实现操作系统的时间管理,linux中实现定时执行程序的功能会用到cron。
编写简单守护进程
任何一个进程都可以将其变为守护进程
create_daemon函数要素:
(1)子进程等待父进程退出
(2)子进程使用setsid创建新的会话期,脱离控制台
(3)调用chdir将当前工作目录设置为‘/’(根目录)
(4)umask设置为0以取消任何文件权限屏蔽
(5)关闭所有文件描述符-->使用函数sysconf
(6)将0、1、2定位到/dev/null
示例代码如下;
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
void create_daemon(){
pid_t pid=-1;
pid = fork();
if (pid>0){
//父进程,我们可以设置直接退出
exit(0);
}
if (pid<0){
perror("fork error");
exit(-1);
}
int p = setsid();
if (p==-1){
perror("set error");
exit(-1);
}
chdir("/");
umask(0);
int i=0;
long len=sysconf(FOPEN_MAX);
for (i=0;i<len;i++){
close(i);
}
open("/dev/null",O_RDWR);
open("/dev/null",O_RDWR);
open("/dev/null",O_RDWR);
}
int main(void){
create_daemon();
while(1){
printf("I am running...\n");
}
return 0;
}
查看进程如下:
此时,该程序成为守护进程,我们无法通过关闭终端来关闭此进程,但我们可以通过kill指令发布信号来终止此进程。
例如:kill -9杀死pid为3480的create_daemo进程。
使用syslog记录调试信息
openlog-> syslog-> closelog
man 3 syslog查看具体使用方法:
(1)一般log信息都在操作系统的/var/log/messages这个文件中存储着,但是ubuntu中是在/var/log/syslog文件中的。
syslog的工作原理:
(1)操作系统中有一个守护进程syslogd (开机运行,关机时结束),这个守护进程syslogd负责进行日志文件的写入和维护。
(2)syslogd是独立于我们任意一个进程而运行的。我们当前进程和syslogd进程本来是没有任何关系的,但我们的当前进程可以通过调用openlog打开一个和syslogd相连接的通道,然后通过syslog向syslogd发消息,然后由syslogd将其写入到日志文件系统中。
(3) syslogd其实就是一个日志文件系统的服务器进程,提供日志服务。任何需要写日志的进程都可以通过openlog/syslog/closelog这三个函数使用syslogd进行写入。
守护进程相关的弊端问题:
守护进程是长时间运行而不退出的进程,守护进程被执行多次时,将会有多个进程占用和浪费系统资源。
这样并不是我们想要的。我们的守护进程一般都是服务器,服务器程序只要运行一个进程则足够,多次同时运行并没有意义甚至会带来错误。
因此我们希望程序具有一个单例运行的功能。也就是当我们执行、运行程序时,若当前还没有运行此程序的进程,则运行,若之前已有一该程序的进程在运行,则本次运行直接退出(提示该程序已经在运行)。
解决此问题的最常见方法:
用一个文件的存在与否作为标志。具体方法为在程序执行前判断相关特定文件是否存在,若存在表示该程序相关进程已运行,不存在表示此程序相关进程还未进行。
我们可以在程序执行时创建该特殊文件(在系统中唯一),运行结束后主动删除此文件,确保程序的单例运行。
示例代码:
#include<stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE "/var/book_test_once"
void delate_file(){
remove(FILE);
}
int main(void){
int fd = -1;
fd=open(FILE,O_RDWR | O_TRUNC | O_CREAT | O_EXCL,0664);
if (fd<0){
if (errno==EEXIST){
printf("file has exits!\n");
}
return -1;
}
atexit(delate_file);
printf("file open success\n");
int i=0;
for (i=0;i<10;i++){
printf("running: %d\n ",i);
sleep(1);
}
return 0;
}
在调试代码的过程中出现了一点小问题,经过排查发现/var目录对我当前的用户没有写(write)的权限,所以我用chmod a+x /var添加了w权限,发现程序可以按预期运行:
查看标志文件在程序运行时和运行结束的存亡状态:
红色箭头为程序运行中,/var/book_test_once被建立
橙色为程序终止后,/var/book_test_once被删除
通过此一系列操作,在进程进行时检测相关标准文件是否存在便可避免多道相同进程同时运行,实现单例运行。
linux的IPC机制----管道
管道(无名管道):
(1)管道通信的原理:内核维护的一块内存,有读端和写端(管道是单向通信的)
(2)管道通信的方法:父进程创建管理后fork子进程,子进程继承父进程的管道fd
(3)管道通信的限制:只能在父子进程间通信、半双工
(4)管道通信的函数: pipe、 write、 read、 close
有名管道(fifo):
(1)有名管道的原理:实质也是内核维护的一块内存,表现形式为一个有名字的文件
(2)有名管道的使用方法:固定-一个文件名,2个进程分别使用mkfifo创建fifo文件,然后分别open打开获取到fd,一个读取,一个写入
(3)管道通信限制:半双工(不限父子进程,任意2个进程都可)
(4)管道通信的函数: mkfifo、open、write、 read、 close