Day 50 Linux(管道)

1. Linux中常见经常间通信方法

1.1 IPC方法

        由于在Linux环境下,进程地址空间都是相互独立,每个进程都会有不同的体制空间。任何一个进程的全局变量在另一个进程中都是看不到的,所以进程和进程之间不能相互访问,要交换数据的话必须要通过内核,Linux会在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC InterProcess Communication)。

Day 50 Linux(管道)_第1张图片

我们在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。

随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者被弃用。现今常用的进程问通信方式有:

(1)管道(使用最简单)。

(2)信号(开销最小)

(3)共享映射区(无血缘关系)。

(4)本地套接字(最稳定)。 

1.2 pipe管道(无名管道)

1.2.1 概念

        管道是Linux进程之间的通信方式,但是只能作用于有血缘关系的进程之间,为他们完成数据传递的功能,它可以把一个程序的输出连接到另一个程序的输出

         调用pipe函数即可创建一个管道,有如下特征:

                (1)他只能作用于父子进程或者兄弟进程之间

                (2)它是一个半双工模式通信,具有固定的写端和读端

                (3)管道也可以看作是一种特殊的文件,对于它的读写使用read和write等函数,但是它不是普通的文件,并不属于其他文件系统且只存在于内存中,内核缓冲区实现

Day 50 Linux(管道)_第2张图片

管道的局限性:

(1) 进程不能自己写数据、读数据

(2)管道中数据不可以反复读,一旦读走了,就不再存在

(3)采用半双工通信方式,数据只能在一个方向上流动

(4)只能在有公共祖先的进程间使用管道

常见的通信方式有,单工通信、半双工通信、全双工通信

Day 50 Linux(管道)_第3张图片

管道是基于文件描述符的通信方式,当一个管道建立的时候,它会创建两个文件描述符fd[0]和fd[1] ,其中fd[0]固定用于读管道,而fd[0]固定用于写管道,这样就构成了一个半双工的通道

Day 50 Linux(管道)_第4张图片 

 管道关闭时只需要将这两个文件描述符关闭即可,可以使用close函数逐个关闭各个文件描述符

1.2.2 pipe函数

创建管道

int pipe(int fd[2]);
//成功返回 0;    f[0]为读端,f[1]为写端
//失败返回 -1,设置error

我们调用了这个函数,如果成功,就会返回r/w两个文件描述符,不用open,但是需要我们手动close

向管道文件读写数据其实就是在读写内核缓冲区

管道创建成功后,创建该管道的进程(父进程)同时掌握着管道的读端和写端,那么我们如何实现父子进程间通信呢?

1. 父进程调用pipe函数创建管道,得到两个文件描述符f[0],f[1]指向管道的读端和写端

2. 父进程调用fork创建子进程,那么子进程也会有两个文件描述符指向同一个管道

3. 父进程关闭管道读端,子进程关闭管道写端,父进程可以往管道中写入数据,子进程将管道中的数据读出来

由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就形成了进程间的通信

父子通信

例:父子进程使用管道通信,父写入字符串,子进程读出并打印到屏幕上

Day 50 Linux(管道)_第5张图片

#include
#include
#include
#include

void sys_err(const char *str)
{
        perror(str);
        exit(1);
}

int main(int argc, char *argv[])
{
        int ret;
        int fd[2];
        pid_t pid;
        char *str = "hello pipe\n";
        char buf[1024] = "";
        ret = pipe(fd);
        if(ret == -1)
        {
                sys_err("pipe error");
        }
        pid = fork();
        if(pid > 0)
        {
                close(fd[0]);
                write(fd[1], str, strlen(str));
                close(fd[1]);
        }
        else if(pid == 0)
        {
                close(fd[1]);
                ret = read(fd[0], buf, sizeof(buf));
                write(STDOUT_FILENO, buf, ret);
                close(fd[0]);
        }
        else
        {
                sys_err("fork error");
        }
        return 0;

}

 练习:创建一个进程两个管道,从一管道写入小写字符串,另一管道读出大写字符串。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
int main() 
{ 
        int fd[2]; //管道1
        int fd1[2]; //管道2
        pid_t pid; 
        char r_buf[100] = ""; //读取
        char w_buf[100] = ""; //写入
        char buf[100]; 
        char *p_wbuf = w_buf; 
        int r_num; 
        if(pipe(fd) < 0) 
        { 
                perror("pipe fd Error"); 
 
        if(pipe(fd1) < 0) 
        { 
                perror("pipe fd1 Error"); 
                exit(-1); 
        } 
        pid = fork(); 
        if(pid == 0) 
        { 
                close(fd[1]); 
                close(fd1[0]); 
                sleep(3); 
 
                if((r_num = read(fd[0], r_buf, 100)) > 0) 
                { 
                } 
                for(i = 0; i < r_num; i++) 
                { 
                        r_buf[i] -= 32; 
                } 
                if(write(fd1[1], r_buf, r_num) == -1) 
                { 
                        printf("写入成功!\n"); 
                } 
 
                close(fd1[1]); 
                close(fd[0]); 
                exit(0); 
        } 
        else if(pid > 0) 
        {
                close(fd[0]);
                close(fd1[1]);
                sleep(1);
                printf("请输入字符串:\n");
                scanf("%s", w_buf);
                write(fd[1], w_buf, strlen(w_buf));

                sleep(1);
                if(r_num = read(fd1[0], w_buf, 100) > 0)
                {
                        printf("读取管道内的内容是:%s\n", w_buf);
                }

                close(fd[1]);
                close(fd1[0]);
                waitpid(pid, NULL, 0);
        }
        return 0;

        
}
       

Day 50 Linux(管道)_第6张图片

 

兄弟进程通信

#include
#include
#include
#include
#include
#define BUFSIZE 1024
void err_quit(char *str)
{
    perror(str);
    exit(1);
}
int main(void)
{
    int fd[2];
    char buf[BUFSIZE];
    pid_t pid;
    int len;
    if((pipe(fd)<0))//创建管道
    {
        err_quit("pipe");
    }
    if((pid=fork())<0)//创建第一个子进程
    {
        err_quit("pipe");
    }
    else if(pid==0)
    {
        close(fd[0]);//关闭读端
        write(fd[1],"hello brother!",14);//子进程1写入信息
        exit(0);
    }
    else
    {
        if((pid=fork())<0)//创建第二个子进程
        {
            err_quit("pipe");
        }
        else if(pid==0)
        {
            close(fd[1]);//关闭写端
            len=read(fd[0],buf,BUFSIZE);//子进程2读取管道内容
            write(STDOUT_FILENO,buf,len);//子进程2写到显示端
            printf("\n");
            exit(0);
        }
        else
        {//父进程关闭无用端口,
            close(fd[0]);
            close(fd[1]);
            sleep(1);
            exit(0);
        }
    }
}

 

 1.2.3 管道读写行为

 Day 50 Linux(管道)_第7张图片

(1)读管道

                1. 管道中有数据:

                        a. read返回实际读到的字节数,一旦数据读走了,就没有数据了。

                        b. 可允许多个读端,但存在竞争关系,速度快的进程读走后,数据就莫得了

                2. 管道中无数据

                        a. 管道写端全部关闭,read返回0

                        b. 写端没有全部被关闭,read阻塞等待

(2)写管道

                1. 管道读端全部被关闭,进程异常终止

                2. 管道读端没有全部被关闭

                        a. 管道已满,write阻塞

                        b. 管道未满,write将数据写入,并返回实际写入的字节数

                        c. 可以允许多个写端,但存在竞争关系,读会阻塞第一个写的进程,一旦读到内容,就读取结束,如果多个写在读之前全部写完,则全部拿出

测试: 写端打开,3s内部写数据,读端阻塞等待数据,3s后接收到数据停止

#include
#include
#include
#include

int main(int argc, char *argv[] )
{
    int ret;
    int fd[2];
    pid_t pid;
    char *str="hello pipe\n";
    char buf[1024]="\0";
    ret = pipe(fd);
    if (ret == -1)
    {
        perror("pipeError");
    }
    pid = fork();
    if(pid>0)
    {
        close(fd[0]);
        sleep(3);
        write(fd[1],str, strlen(str));
        close(fd[1]);
    }
        else if(pid== 0)
        {
            close(fd[1]);
            ret=read(fd[0],buf, sizeof(buf));
            write(STDOUT_FILENO,buf,ret);
            close(fd[0]);
        }
    else
    {
        perror("forkError");
    }
    return 0;
}

Day 50 Linux(管道)_第8张图片

1.2.4 标准流管道popen()函数

        这个函数和Linux的文件操作符中有基于文件流的标准I/O操作一样,管道的操作也指出基于文件流的模式。这种基于文件流的管道,主要是用来创建一个连接到另一个进程的管道,这里的"另一个进程"也就是一个可以进 行一定操作的可执行文件,例如,用户执行”ls -l”或者自己编写的程序”./pipe” 等

        由于这类操作很常用,因此标准流管道就将一系列的创建过程合并到一个函数 popen()中完成,它所完 成的工作有以下几步:

(1)创建一个管道

(2)fork()创建一个子进程

(3)在父子进程中关闭不需要的文件描述符

(4)执行exec函数族调用

(5)执行函数中所指定的命令

        这个函数的使用可以大大减少代码的编写量,但同时也有一些不利之处。例如,它不如前面管道创建的 函数那样灵活多变,并且用popen()创建的管道必须使用标准I/O函数进行操作,而不能使用系统调用的 read()、write()一类不带缓冲的I/O函数。与之相对应,关闭用popen()创建的流管道必须使用函数 pclose(),该函数关闭标准I/O流,并等待命令执行结束。

FILE * popen( const char * command,const char * type);

如果 type 为 r,那么调用进程读进 command 的标准输出stdout。

如果 type 为 w,那么调用进程写到 command 的标准输入stdin。

若成功则返回文件指针,否则返回NULL,错误原因存于errno中。

 popen:创建一个管道

pclose:关闭由popen()打开的管道

例:调用shell命令ls来打印当前目录信息到显示器程序

Day 50 Linux(管道)_第9张图片

 

#include 
#include 
#include 
#include 
#define BUFSIZE 1024
int main()
{
    FILE *fp;
    char *cmd= "ls -l";
    char buf[BUFSIZE];
    if(NULL == (fp = popen(cmd, "r")))
    {
        printf("Popen errorl\n");
        exit(1);
    }
    while(NULL != (fgets(buf, BUFSIZE, fp)))
    {
        printf("%s",buf);
    }
    printf("\n");
    pclose(fp);
    exit(0);
}

Day 50 Linux(管道)_第10张图片 

例:调用shell命令来cat打印buf里面的内容

Day 50 Linux(管道)_第11张图片 

#include
#include
#include
#include
#define Buffer 100
int main()
{
    FILE *write_fp;
    char buffer[Buffer + 1];
    //把格式化的数据写入某个字符串缓冲区。
    //返回值:字符串长度( strlen)
    sprintf(buffer,"once upon a time, there was...\n");
    /*******格式化输出文件中的数据********///
    //-c:等价于-t c, 选择ASCII码字符或者是转义字符
    write_fp=popen("cat > 1.txt","w");
    if(write_fp != NULL)
    {
        fwrite( buffer, sizeof(char) , strlen(buffer),write_fp);
        pclose(write_fp);
    }
}

 1.3 FIFO有名管道

1.3.1 有名管道简介

FIFO是Linux基础文件类型的一种,但是,FIFO文件在磁盘上是没有数据块的,仅仅是用来标识内核中的一条通道。各进程可以打开这个文件进行read/write,实际上实在读写内核通道,那么这样就算是实现了进程之间的通信

有名管道(FIFO)是对无名管道的一种改进,它有以下特点:

(1)它可以使互不相关的两个进程间实现彼此通信

(2)该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当做普通文件一样进行读写操作,使用非常方便

(3)FIFO严格地遵循先进先出规则,对管道及FIFO的读总是从开始处返回数据,对它们的写则是把数 据添加到末尾,它们不支持如 lseek()等文件定位操作

1.3.2 FIFO创建方式

int mkfifo(const char *pathname,mode_t mode);

//成功: 0;失败: -1

有名管道创建可以使用函数mkfifo(),该函数类似于文件中的open操作,可以指定管道的路径和打开的模式

例:利用mkfifo函数创建FIFO管道

#include
#include
#include
#include
#include
#define BUFSIZE 1024

int main(void)
{
    int ret = mkfifo("mytestfifo", 0664);
    if(ret==-1)
    {
        err_quit("mkfifo error");
    }    
    return 0;
}

Day 50 Linux(管道)_第12张图片

我们也可以在命令函使用“mkfifo 管道名”来创建有名管道 

 Day 50 Linux(管道)_第13张图片

1.3.3 FIFO通信使用

         我们在创建管道成功后,就能使用open、write、read这些函数了。对于为读二打开的管道可在open中设置O_RDONLY,对于为写二打开的管道可以在open中设置O_WRONLY

默认方式:

        1. 如果FIFO读没有打开,无法写入内容,write可能处于阻塞状态,当读打开之后,就会立即写入内 容,或者成功write一次后,自动退出

         2. 如果FIFO写没有打开,无法读入内容,read处于阻塞状态,当写如内容后,就会立即读到         

        3. 当unlink()取消有名管道后,write会自动停止,read可能也会停止    

例:wrfifo.c 负责写入内容,rdfifo.c负责读出内容

#include
#include
#include
#include
#include
#include
#define BUFSIZE 1024
void err_quit(char *str)
{
    perror(str);
    exit(1);
}
int main(int argc, char *argv[])
{
    int fd, i;
    char buf[4096];
    if(argc<2)
    {
        printf("Enter like this: ./a.out fifoname\n");
        return -1;
    }
    fd = open(argv[1],O_WRONLY);
    if(fd<0)
    {
        err_quit("open");
    }
    i=0;
    for(;i<5;i++)
    {
        sprintf(buf, "hello haifeng %d\n",i+1);
        int ret=write(fd, buf, strlen(buf));
        printf("write:%s ret=%d\n",buf,ret);
        sleep(1);
    }
    close(fd);
    unlink("testfifo");
    return 0;
}
#include
#include
#include
#include
#include
#define BUFSIZE 1024
void err_quit(char *str)
{
    perror(str);
    exit(1);
}
int main(int argc, char *argv[])
{
    int fd, len;
    char buf[4096];
    if(argc<2)
    {
        printf("Enter like this: ./a.out fifonane\n");
        return -1;
    }
    fd = open(argv[1], O_RDONLY);
    if(fd<0)
    {
        err_quit("open");
    }
    int i=0;
    for(;i<10;i++)
    {
        len=read(fd,buf,sizeof(buf));
        write(STDOUT_FILENO, buf, len);
        sleep(1);
    }
    close(fd);
    unlink("testfifo");
    return 0;
}

1.3.4 对于读可以设置阻塞和非阻塞

        与普通文件不同的是阻塞问题,由于普通文件在读写的时候不会出现阻塞问题,但是在管道的读写中却有阻塞的可能,这里的非阻塞标志可以在open()函数中设定O_NONBLOCK,下面分别对阻塞打开和非阻塞打开的读写进行讨论

对于读进程

(1)若该管道是阻塞打开,且当前FIFO内没有数据,则对读进程而言将一直阻塞到有据写入。 (2)若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败。

Day 50 Linux(管道)_第14张图片

1.3.5 access函数确定访问权限

        access()函数的功能是确定文件或文件夹的访问权限,即检查某个文件的存取方式,比如说是只读方式、 只写方式等。如果指定的存取方式有效,则函数返回0,否则函数返回-1。

 Day 50 Linux(管道)_第15张图片

 例:access_wfifo.c负责些内容

#include 
#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    int fd;
    int w_num;
    int ret=mkfifo("myfifo",0664);
    printf("mkfifo() ret=%d\n",ret);
    if(access("myfifo",F_OK))
    {
        printf("myfifo no exist\n");
        return 1;
    }
    if (access("myfifo",W_OK) < 0)
    {    
        printf("cannot write fifo.....\n");
    return 1;
    }
    fd = open("myfifo", O_WRONLY);
    if(fd==-1)
    {
        printf("open %s for write error\n", "myfifo");
        return 1;
    }
    w_num= write(fd,"write hello access fifo", strlen("write hello access fifo"));
    printf("ret=%d--%s--\n", w_num, "write hello access fifo");
    // unlink("testfifo"); //任意一个程序删除FIFO都行
    return 0;
}

 access_rfifo.c负责读内容

#include 
#include 
#include 
#include 
#include 
#include 
#define FILEMODE (S_IRUSR| S_IWUSR)
int main()
{
    char r_buf[50];
    int fd;
    int r_num;
    if (access("myfifo",R_OK) < 0)
    {
        printf("cannot read fifo.....\n");
        return 1;
    }
    fd = open("myfifo", O_RDONLY);
    if(fd==-1)
    {
        printf("open %s for read error\n", "myfifo");
        return 1;
    }
    r_num= read(fd,r_buf, 50);
    printf("Get %d bytes**%s**\n", r_num, r_buf);
    // unlink("testfifo");//任意一个程序删除FIFO都行
    return 0;
}

你可能感兴趣的:(linux,运维,服务器)