Linux下多任务间通信和同步-管道

时间:2021-01-14 08:08:18

Linux下多任务间通信-管道

嵌入式开发交流群280352802,欢迎加入!

1.管道简介

管道式Linux系统中最古老的进程间通信机制,这里所说的管道是指无名管道(PIPE),它可用于具有亲缘关系进程间的通信.有名管道(FIFO)克服了管道没有名字的限制,因此,除了具有管道所有具有的功能外,它还允许无亲缘关系进程间的通信.Linux的管道主要包括两种:无名管道和有名管道,本文主要介绍这两种管道.

把一个进程连接到另一个进程的一个数据流称为"管道".比如:ls|more.这条命令的作用是列出当前目录下的所有文件盒子目录,如果内容超过了一页则自动进行分页.符号"|"就是shell为ls和more命令简历的一条管道,它将ls的输出直接送进了more的输入.

无名管道特点
  • 它只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间).
  • 它是一个半双工的通信模式,具有固定的读端和写端.
  • 管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read(),write()等函数.但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中.
有名管道特点
  • 它可以使互不相关的两个进程实现彼此通信.
  • 该管道可以通过路径名来指出,并且在文件系统中是可见的.在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便.
  • FIFO严格地遵循先进先出规则,对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如lseek()等文件定位操作.

2.无名管道

2.1.管道创建和关闭

管道是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fds[0]和fds[1],其中fds[0]固定用于读管道,而fd[1]固定用于写管道,这样就构成了一个半双工的通道.管道关闭时只需将这两个文件描述符关闭即可,可使用普通的close()函数逐个关闭各个文件描述符. 创建管道可以通过调用pipe()来实现.其语法如下:
Linux下多任务间通信和同步-管道
由于得到的是文件描述符,一般文件的I/O函数都可以用于管道,如close,read,write等等.下面我们通过一个简单的例子来演示管道的I/O操作.
#include <stdio.h>#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv)
{
static const char mesg[] = "Hello world!";
char buf[BUFSIZ];
ssize_t rcount, wcount;
int pipefd[2];
size_t len;

/*创建管道*/
if (pipe(pipefd) < 0) {
fprintf(stderr, "%s: pipe failed: %s\n", argv[0],
strerror(errno));
exit(1);
}
printf("PIPE: Read end = fd %d, write end = fd %d\n",
pipefd[0], pipefd[1]);

/*写管道*/
len = strlen(mesg);
if ((wcount = write(pipefd[1], mesg, len)) != len) {
fprintf(stderr, "%s: write failed: %s\n", argv[0],
strerror(errno));
exit(1);
}
printf("Write <%s> via pipe\n", mesg);

/*读管道*/
if ((rcount = read(pipefd[0], buf, BUFSIZ)) != wcount) {
fprintf(stderr, "%s: read failed: %s\n", argv[0],
strerror(errno));
exit(1);
}

buf[rcount] = '\0';
/*添加字符串终止字符*/
printf("Read <%s> from pipe\n", buf);

/*关闭管道*/
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
程序的执行结果如下:Linux下多任务间通信和同步-管道上面的这段程序没有什么有用的功能,只是演示了基本的概念.该程序所建立的管道如下图所示.需要注意的是:逛到的容量十分有限,在<limits.h>头文件中定义的常量PIPE_BUF规定的管道一次传送的最大字节数(4096).
Linux下多任务间通信和同步-管道


2.2多进程中的管道通信

通常不会为了数据通信而在单个进程内使用管道,而是在父子进程中建立管道传递数据,下面我们看一个简单的例子.
#include <stdio.h>#include <errno.h>#include <unistd.h>#include <sys/types.h>#include <stdlib.h>#include <string.h>int main(int argc, char **argv){static const char mesg[] = "Hello world!";intpipefd[2];pid_tpid;char    buf[BUFSIZ];size_t  len,n;if (pipe(pipefd) < 0)perror("pipe error");if ( (pid = fork()) < 0)perror("fork error");    /*父进程向子进程发数据*/else if (pid > 0){/* parent */    printf("PIPE(parent): Read end = fd %d, write end = fd %d\n",    pipefd[0], pipefd[1]);        /*关闭父进程的读端口*/    close(pipefd[0]);        /*写管道*/    len = strlen(mesg);    if (write(pipefd[1], mesg, len) != len)     {    fprintf(stderr, "%s: write failed: %s\n", argv[0],    strerror(errno));    exit(1);    }    printf("Write <%s> to pipe\n", mesg);    close(pipefd[1]);}else{/* child */    printf("PIPE(child): Read end = fd %d, write end = fd %d\n",    pipefd[0], pipefd[1]);        /*关闭子进程的写端口*/    close(pipefd[1]);        /*读管道*/    n = read(pipefd[0], buf, BUFSIZ);    buf[n] = '\0';    printf("Read <%s> from pipe\n", buf);    close(pipefd[0]);}exit(0);}
在上面的例子中,我们建立一条从父进程到子进程的管道,需要在父进程中关闭管道的读端(pipe[0]),在子进程中关闭管道的写段(pipe[1]),如下图所示:
Linux下多任务间通信和同步-管道
程序执行的结果如下:Linux下多任务间通信和同步-管道
fork后父进程首先执行,父进程向管道写入数据;子进程运行后,从管道中读取数据.

管道读写注意点:
  • 只有在管道的读端存在时,向管道写入数据才有意义.否则,向管道写入数据的进程将收到内核传来的SIGPIPE信号(通常为Broken pipe错误).
  • 向管道写入数据时,Linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据.如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞.
  • 父子进程在运行时,它们的先后次序并不能保证,因此,在为了保证父子进程已经关闭了相应的文件描述符,可在两个进程中调用sleep()函数,当然这种调用不是很好的解决方法,在后面学到进程之间的同步与互斥机制之后.
对于第一条注意事项,我们写一个简单的例子进行验证:
#include <stdio.h>#include <errno.h>#include <unistd.h>#include <stdlib.h>#include <string.h>#include <signal.h>static void sig_pipe(int signo){printf("SIGPIPE!\n");exit(0);}int main(int argc, char **argv){static const char mesg[] = "Hello world!";char buf[BUFSIZ];ssize_t rcount, wcount;int pipefd[2];size_t len;if (signal(SIGPIPE,sig_pipe)==SIG_ERR)printf("signal(SIGPIPE) ERROR!\n");/*创建管道*/if (pipe(pipefd) < 0) {fprintf(stderr, "%s: pipe failed: %s\n", argv[0],strerror(errno));exit(1);}printf("PIPE: Read end = fd %d, write end = fd %d\n",pipefd[0], pipefd[1]);close(pipefd[0]);//关闭读端/*写管道*/len = strlen(mesg);if (write(pipefd[1], mesg, len) != len)fprintf(stderr,"%s:write failed:%s\n",argv[0],strerror(errno));printf("Write <%s> via pipe\n", mesg);close(pipefd[1]);return 0;}
程序运行的结果如下:
Linux下多任务间通信和同步-管道

2.3标准流管道

与Linux的文件操作中有基于文件流的标准I/O操作一样,管道的操作也支持基于文件流的模式.这种基于文件流的管道主要是用来创建一个连接到另一个进程的管道,这里的"另一个进程"也就是一个可以进行一定操作的可执行文件.
标准流管道就将一系列的创建过程合并到一个函数popen()中完成.它所完成的工作有以下几步:
  • 创建一个管道;
  • fork()一个子进程;
  • 在父子进程中关闭不需要的文件描述符;
  • 执行exec函数族调用;
  • 执行函数中所指定的命令.
popen函数格式:Linux下多任务间通信和同步-管道pclose函数格式:
Linux下多任务间通信和同步-管道
该函数简化了代码的编写,但有一些不利之处,例如它没有pipe函数的灵活多样,而且启动一个程序还要启动一个shell,消耗的系统资源较多,下面我们看一个简单的例子.
#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <string.h>int main(){    FILE *read_fp;    char buffer[BUFSIZ];    int chars_read;    memset(buffer, '\0', sizeof(buffer));    /*read_fp指针为可读的,连到了”uname-a”命令的标准输出*/    read_fp = popen("uname -a", "r");    if (read_fp != NULL)    {        /*读出命令的执行结果*/        chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);        if (chars_read > 0) {            printf("Output was:-\n%s\n", buffer);        }        pclose(read_fp);        exit(0);    }    exit(1);}
运行的结果如下:
Linux下多任务间通信和同步-管道
实际上相当于在终端上执行如下命令:uname -apopen函数将command参数以下列方式执行/bin/sh -c command这意味着只要是合理的命令行,都可以作为popen的command参数.另一个需要注意的是:由于popen函数返回的是FILE指针,因而不能使用read,write灯不带缓冲的I/O函数,只能使用fread,fget,fwrite等支持FILE类型的函数.最后,因使用pclose函数关闭popen建立的管道.

3.有名管道(FIFO)

管道应用的一个重大限制它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(Nameed pipe或FIFO)提出后,该限制得到了客服.FIFO不同于管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中.这样,即使FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO,不相关的进程也能交换数据.值得注意的是,FIFO严格遵守先进先出(First In First Out),对管道及FIFO的读总是从开始处返回数据,对它们的写则是把数据添加到末尾.

3.1FIFO的创建

通过mkfifo()创建有名管道,可以指定管道的路径和打开的模式.mkfifo()函数语法如下:
Linux下多任务间通信和同步-管道FIFO相关出错信息
Linux下多任务间通信和同步-管道有名管道比管道多了一个打开操作.有两种可能的结构.有两种可能的情况:
  • 如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志).
  • 如果当前打开操作时为写而打开FIFO时,若已经有相应进程为读而打开该FIFO,则当前打开操作成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误()当前打开操作没有设置阻塞标志.
下面我们看一个例子,该例子演示了FIFO的打开操作.
#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <string.h>#include <fcntl.h>#include <sys/types.h>#include <sys/stat.h>/*FIFO文件的路径*/#define FIFO_NAME "/tmp/my_fifo"int main(int argc, char *argv[]){    int res;    int open_mode = 0;    int i;    /*检查命令行参数应该为只读(RO)、只写(WO)、非阻塞(NB)*/    if (argc < 2)     {        fprintf(stderr, "Usage: %s <RO WO NB>\n", *argv);        exit(1);    }    /*处理命令行参数*/    for(i = 1;i<argc ;i++)    {    if (!strncmp(argv[i], "RO", 2))    open_mode |= O_RDONLY;    if (!strncmp(argv[i], "WO", 2))    open_mode |= O_WRONLY;    if (!strncmp(argv[i], "NB", 2))    open_mode |= O_NONBLOCK;     }    /*创建FIFO*/    if (access(FIFO_NAME, F_OK) == -1)     {        res = mkfifo(FIFO_NAME, 0777);        if (res != 0) {            fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);            exit(1);        }    }    /*打开FIFO*/    printf("Process %d opening FIFO\n", getpid());    res = open(FIFO_NAME, open_mode);    printf("Process %d result %d\n", getpid(), res);    sleep(5);    if (res != -1) close(res);    printf("Process %d finished\n", getpid());    exit(0);}
下面我们看看程序执行的结果:
Linux下多任务间通信和同步-管道
第一个fifo_test以读进程驱动,在open调用里等待,当第二个程序打开FIFO文件时,两个进程都将开始继续执行.注意:读进程和写进程是在open调用出得到同步.这种采用的是阻塞模式,当进程被阻塞时,它是不消耗CPU和资源的,所以这种模式从CPU的角度来看非常有效.非阻塞模式就分析了...

3.2FIFO的读写规则

  • 从FIFO中读取数据遵守以下规则
    • 如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞.对于没有设置阻塞标志的读操作来说则返回-1,当当前errno设为EAGAIN,提示以后再试.
    • 对于设置了阻塞标志的读操作而言,造成了阻塞的原因有两个.一个是当前FIFO内有数据,但有其他进程在读这些数据;另外就是FIFO内没有数据.解除阻塞的原因则是FIFO中有新数据写入,不论其写入的数据量的大小,也不论读操作请求多少数据量.
    • 读打开的阻塞标志只对本进程的第一个读操作施加作用;如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其他将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0).
    • 如果没有写进程打开FIFO,则设置了阻塞标志的读操作会阻塞.
    • 注意:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量.

  • 向FIFO中写入数据遵守以下规
  • 对于设置了阻塞标志的写操作
    • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性.如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,知道当缓冲区中能够容纳要写入的字节数时,才开始进行一次性的写操作.
    • 当要写入的数据量大于PIPE_BUF时,linux将不保证写入的原子性.FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完说有请求写的数据后返回.
  • 对于没有设置阻塞标志的写操作
    • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性.在写满所有FIFO空闲缓冲区后,写操作返回.
    • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性.如果当前FIFO空闲缓冲能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写.

3.3FIFO的一个实际应用例子

下面我们用两个程序演示用FIFO实现进程间通信.一个是"数据加工厂",负责常见管道,并向管道写入数据;另一个是"数据消费者",其作用是从FIFO文件读取数据并丢弃它们.
/**************************************************************************************//*简介:数据加工厂 *//*************************************************************************************/#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <string.h>#include <fcntl.h>#include <limits.h>#include <sys/types.h>#include <sys/stat.h>#define FIFO_NAME "/tmp/my_fifo"#define BUFFER_SIZE PIPE_BUF#define LEN_MEG (1024 * 1024 * 10)int main(){    int pipe_fd;    int res;    int open_mode = O_WRONLY;    int bytes_sent = 0;    char buffer[BUFFER_SIZE + 1];    if (access(FIFO_NAME, F_OK) == -1)    {        res = mkfifo(FIFO_NAME, 0777);        if (res != 0){            fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);            return 1;        }    }    printf("Process %d opening FIFO O_WRONLY\n", getpid());    pipe_fd = open(FIFO_NAME, open_mode);    printf("Process %d result %d\n", getpid(), pipe_fd);    if (pipe_fd != -1)     {        while(bytes_sent < LEN_MEG) {            res = write(pipe_fd, buffer, BUFFER_SIZE);            if (res == -1)    {                fprintf(stderr, "Write error on pipe\n");                return 1;            }            bytes_sent += res;        }        (void)close(pipe_fd);     }    else     {        return 1;            }    printf("Process %d finished\n", getpid());    return 0;}
该程序编译成fifo_creator,下面的程序编译为fifo_reader.
/**************************************************************************************//*简介:数据消费者 *//*************************************************************************************/#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <string.h>#include <fcntl.h>#include <limits.h>#include <sys/types.h>#include <sys/stat.h>#define FIFO_NAME "/tmp/my_fifo"#define BUFFER_SIZE PIPE_BUFint main(){    int pipe_fd;    int res;    int open_mode = O_RDONLY;    char buffer[BUFFER_SIZE + 1];    int bytes_read = 0;    memset(buffer, '\0', sizeof(buffer));        printf("Process %d opening FIFO O_RDONLY\n", getpid());    pipe_fd = open(FIFO_NAME, open_mode);    printf("Process %d result %d\n", getpid(), pipe_fd);    if (pipe_fd != -1)     {        do {            res = read(pipe_fd, buffer, BUFFER_SIZE);            bytes_read += res;        } while (res > 0);        close(pipe_fd);    }    else     {        return 0;    }    printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);    return 1;}
程序的执行结果如下:
Linux下多任务间通信和同步-管道两个程序都使用阻塞模式的FIFO文件.先启动fifo_creator,它将阻塞并等待有读进程打开这个FIFO文件.当fifo_reader启动的时候,写进程解除阻塞状态,并开始向管道写入数据.同时,读进程也开始从管道读取数据.不必考虑他们之间的同步问题,系统会为这两个进程安排好它们的时间分配情况,使写进程在管道满的时候阻塞,而读进程在管道满的时候阻塞,而读进程在管道空的时候阻塞. 在执行上面这个例子的时候,会发现它们很快就结束了,实际上在它们的运行期间,这两个进程通过管道已经传输了10MB的数据,这个速度是相当快的.这说明系统针对FIFO做了优化,这是一种高效的进程间传输数据的办法.