Linux学习笔记——进程间的通信-管道

1.管道是什么?为什么要有管道

操作系统将一个程序的输出交给另一个程序处理,操作可以使用输入输出重定向加文件。

    zach@zach-i16:/$ sudo ls -l / > test.txt
    zach@zach-i16:/$ wc -l test.txt
    26 test.txt
    // Linux系统中的wc(Word Count)命令的功能为统计指定文件中的字节数、字数、行数,并将统计结果显示输出
    //-c 统计字节数。
    //-l 统计行数。
    //-m 统计字符数。这个标志不能与 -c 标志一起使用。
    //-w 统计字数。一个字被定义为由空白、跳格或换行字符分隔的字符串。
    //-L 打印最长行的长度。
    //-help 显示帮助信息
    //--version 显示版本信息 

显得很麻烦,所以,管道概念产生。
任何一个shell中,可以使用“|”连接两个命令,shell会将前后两个进程的输入输出用一个管道相连。

    zach@zach-i16:/$ sudo ls -l | wc -l
    26

管道本质上就是一个文件,前面的进程以写的方式打开文件,后面的进程以读方式打开,前面写完后面读,实现了通信。
虽然实现形态上是文件,但管道本身并不占用磁盘或者其他外部储存空间,
Linux实现上占用内存空间。
Linux上的管道就是一个操作方式为文件的内存缓冲区。

2.管道的分类和使用

(1)匿名管道:

最常见的形态就是我们在shell操作中最常用的“|”。
特点是只能在父进程中使用,父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。
除父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。保证了传输数据的安全性,同时却降低了管道的通用性。

(2)命名管道:

可以使用mkfifo命令来创建一个命名管道,跟创建一个文件没有什么区别

    zach@zach-i16:~$ mkfifo pipe
    zach@zach-i16:~$ ls -l pipe
    prw-r--r-- 1 zach zach 0 329 11:35 pipe

创建出来的文件类型是p类型,表示这是一个管道文件。
有了这个管道文件,系统中就有了对一个管道的全局名称,任何两个不相关的进程都可以通过这个管道文件进行通信。

For example:让一个进程写这个管道文件

    zach@zach-i16:~$ echo hello > pipe

此时这个写操作会阻塞,因为管道另一端没有人读。这是内核对管道文件定义的默认行为。
此时有进程读这个管道,那么这个写操作的阻塞才会解除:

    zach@zach-i16:~$ cat pipe
    hello

cat完这个文件后,另一端的echo命令也就返回了。

Linux系统无论对于命名管道和匿名管道,底层都用的是同一种文件系统的操作行为,这种文件系统叫pipefs,在/proc/filesystems文件中找到系统是不是支持这种文件系统:

    zach@zach-i16:/$ cat /proc/filesystems | grep pipefs
    nodev   pipefs

3.系统编程中使用管道

(1)匿名管道(PIPE):

创建匿名管道系统调用pipe();
创建匿名管道的函数mkfifo();//头文件是 unistd.h
一个进程中使用管道:

    #include 
    #include 
    #include 
    #include 

    #define str "hello world"

    int main()
    {
        int pipefile[2];//pipefile[0]是读方式打开,作为管道读描述符;pipefile[1]是写方式打开,作为管道的写描述符
    char buf[BUFSIZ];//BUFSIZ代表默认缓冲大小,8192

    if(pipe(pipefile)==-1)//int pipe(int filedes[2]);若成功则返回零,否则返回-1错误原因存于errno中
    {
        perror("pipe()");//perror ( )用来将上一个函数发生错误的原因输出到标准设备
        exit(1);//异常退出
    }

    if(write(pipefile[1],str,strlen(str))<0)//write函数将str中的strlen(str)字节内容写入文件描述符pipefile[1].成功时返回写的字节数.失败时返回-1. 并设置errno变量     
    {
        perror("write()");
        exit(1);
    }
    if(read(pipefile[0],buf,BUFSIZ)<0)// read函数是负责从pipefile[0]中读取内容.成功时,read返回实际所读的字节数,如果返回的值是0,表示已经读到文件的结束了.小于0表示出现了错误
    {
        perror("write()");
        exit(1);
    }
    printf("%s\n",buf);//打印读取内容
    exit(0);//正常退出
    }

运行结果:

    zach@zach-i16:~/文档/note/Linux/进程通信/1.管道$ gcc pipe.c -o pipe
    zach@zach-i16:~/文档/note/Linux/进程通信/1.管道$ ./pipe
    hello world

程序创建了一个管道,对管道写了一个字符串之后从管道获取,打印出来。
双进程通信使用管道(半双工):

    #include 
    #include 
    #include 
    #include 
    #include //提供类型pid_t的定义
    #include //提供wait()函数定义

    #define str "hello,world"

    int main()
    {
        int pipefile[2];
        pid_t pid;//实际上就是int类型
        char buf[BUFSIZ];
        if(pipe(pipefile)==-1)
        {
            perror("pipe()");
            exit(1);
        }
        pid=fork();//在父进程中,fork返回新创建子进程的进程ID;在子进程中,fork返回0;如果出现错误,fork返回一个负值
        if(pid==-1)//出现错误
        {
            perror("fork()");
            exit(1);
        }
        if(pid==0)//子进程返回
        {
            printf("Child pid is : %d\n", getpid());//getpid ()用来取得目前进程的进程识别码
            if(read(pipefile[0],buf,BUFSIZ)<0)
            {
                perror("write()");
                exit(1);
            }
            printf("%s\n",buf);
            bzero(buf,BUFSIZ);//bzero() 会将内存块(字符串)的前n个字节清零,其原型为:void bzero(void *s, int n);
            snprintf(buf,BUFSIZ,"Message from child: My pid is : %d",getpid());//buf为要写入的字符串;BUFSIZ为要写入的字符的最大数目,超过会被截断;成功则返回参数buf字符串长度,失败则返回-1,错误原因存于errno 中
            if(write(pipefile[1],buf,strlen(buf))<0)
            {
                perror("write()");
                exit(1);
            }
        }
        else//父进程
        {
            printf("Parent pid is : %d\n",getpid());
            snprintf(buf,BUFSIZ,"Message from parent :My pid is : %d",getpid());
            if(write(pipefile[1],buf,strlen(buf))<0)
            {
                perror("write()");
                exit(1);
            }
            sleep(1);//挂起1秒
            bzero(buf,BUFSIZ);
            if(read(pipefile[0],buf,BUFSIZ)<0)
            {
                perror("write()");
                exit(1);
            }
            printf("%s\n",buf);
            wait(NULL);//等待子进程退出,NULL的意思是退出状态不关注。
        }
        exit(0);
    }

运行:

    zachh@zach-i16:~/文档/note/Linux/进程通信/1.管道$ gcc pipen.c -o pipen
    zach@zach-i16:~/文档/note/Linux/进程通信/1.管道$ ./pipen
    Parent pid is : 5099
    Child pid is : 5100
    Message from parent :My pid is : 5099
    Message from child: My pid is : 5100

使用同一管道,父子进程可以分时给对方发送消息。
半双工情况下,管道两端都可能多个进程进行读写处理,情况比较复杂。
推荐两个进程通信(单工模式,一个进程只读管道,一个进程只写管道):

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 

    #define str "hello world"

    int main()
    {
        int pipefile[2];
        pid_t pid;
        char buf[BUFSIZ];
        if(pipe(pipefile)==-1)
        {
            perror("pipe()");
            exit(1);
        }
        pid=fork();
        if(pid==-1)
        {
            perror("fork()");
            exit(1);
        }
        if(pid==0)
        {
            close(pipefile[1]);
            printf("Child pid is : %d\n",getpid());
            if(read(pipefile[0],buf,BUFSIZ)<0)
            {
                perror("read()");
                exit(1);
            }
            printf("%s\n",buf);
        }
        else
        {
            close(pipefile[0]);
            printf("Parent pid is : %d\n",getpid());
            snprintf(buf,BUFSIZ,"Message from parent: My pid is: %d",getpid());
            if(write(pipefile[1],buf,strlen(buf))<0)
            {
                perror("write()");
                exit(1);
            }
            wait(NULL);
        }
        exit(0);
    }

运行情况如下:

    zach@zach-i16:~/文档/note/Linux/进程通信/1.管道$ gcc pipenn.c -o pipenn
    zach@zach-i16:~/文档/note/Linux/进程通信/1.管道$ ./pipenn
    Parent pid is : 5309
    Child pid is : 5310
    Message from parent: My pid is: 5309

(2)命名管道(FIFO):

使用mkfifo函数和nknod系统调用,创建命名管道

    #include 
    #include 
    #include 
    #include //在/usr/include/linux/stat.h中声明了S_IFIFO=0010000

    int main(int argc,char *argv[])
    {
        if(argc!=3)
        {
            fprintf(stderr,"Argument error\n");//stderr标准错误输出设备
            exit(1);
        }
        if(mkfifo(argv[1],0600)<0)//返回值:成功,0;失败,-1;参数argv[1]是创建FIFO路径,参数mode指定创建的FIFO访问模式,0600==-rw-------
        //第二个参数:
        //owner=rwx=4+2+1=7
        //group=rw-=4+2+0=6
        //others=---=0+0+0=0
        {
            perror("mkfifo()");
            exit(1);
        }

        if(mknod(argv[2],0600|S_IFIFO,0)<0)//第一个参数表示你要创建的文件的名称,第二个参数表示文件类型,第三个参数表示该文件对应的设备文件的设备号。只有当文件类型为 S_IFCHR 或 S_IFBLK 的时候该文件才有设备号,创建普通文件时传入0即可
        {
            perror("mknod()");
            exit(1);
        }
        exit(0);
    }

运行情况:

    zach@zach-i16:~/文档/note/Linux/进程通信/1.管道$ ls
    note  pipe  pipe.c  pipen  pipen.c  pipenn  pipenn.c  pipennn  pipennn.c
    zach@zach-i16:~/文档/note/Linux/进程通信/1.管道$ ./pipennn pipefile1 pipefile2
    zach@zach-i16:~/文档/note/Linux/进程通信/1.管道$ ls
    note  pipe.c     pipefile2  pipen.c  pipenn.c  pipennn.c
    pipe  pipefile1  pipen      pipenn   pipennn

创建完后,便可以使用read(),write(),open()等操作,操作与匿名管道相似。

4.小结

本次Linux进程间通信的学习是基于学校里操作系统老师在微信里分享的一篇博文

穷佐罗的Linux书-Linux的进程间通信 - 管道
微博ID:orroz
微信公众号:Linux系统技术

本篇博文全是自己对学习那篇文章的笔记,文章中代码里函数的具体参数和含义都一个个查询资料并且搞清,基本把一些初学者可能会有困惑的地方注释在了代码块里,并且把自己认为有用的东西写了出来,仅供参考

你可能感兴趣的:(linux学习笔记)