进程间通信

1.管道

 

对于具有公共祖先的进程,其管道是建立在3-4G的内核空间中的。每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

 

调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe函数调用成功返回0,调用失败返回-1。

#include <sys/wait.h>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>                                                           
#include<stdlib.h>
#include<string.h>
int main(void)
{
    pid_t pid; 
    int fd[2];
    if(pipe(fd)<0)
    {
        perror("pipe");
        exit(1);
    }
    printf("fd[0]=%d, fd[1]= %d\n",fd[0],fd[1]);
    if((pid=fork())<0)
        {
            perror("fork");
            exit(1);
        }
    else if(pid==0)//child
    {
        char c_str[1024];
        int n;
        close(fd[1]);//关闭写端口
        n=read(fd[0],c_str,sizeof(c_str)/sizeof(c_str[0]));//由于不知道读多少,所以读取最大长度
        close(fd[0]);
        write(STDOUT_FILENO,c_str,n);
    }
    else//parents
    {
        char str[]="hello pipe!\n";
        sleep(2);
        close(fd[0]);//关闭读端口
        write(fd[1],str,strlen(str));
        close(fd[1]);
        wait(NULL);//等待回收子进程资源
    }
    return 0;
}

 

在父进程没有传输数据在管道中时,子进程中的read函数会阻塞等待。我们可以使用fcntl函数改变一个已经打开文件的属性,如重新设置读、写、追加、非阻塞等标志。

#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>                                                           
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
int main(void)
{
    pid_t pid; 
    int fd[2];
    if(pipe(fd)<0)
    {
        perror("pipe");
        exit(1);
    }
    printf("fd[0]=%d, fd[1]= %d\n",fd[0],fd[1]);
    if((pid=fork())<0)
        {
            perror("fork");
            exit(1);
        }
    else if(pid==0)//child
    {
        char c_str[1024];
        int n,flags;
        flags=fcntl(fd[0],F_GETFL);
        flags |=O_NONBLOCK;
        if(fcntl(fd[0],F_SETFL,flags)==-1)
        {
            perror("fcntl");
            exit(1);
        }
        close(fd[1]);//关闭写端口
tryagain:
        n=read(fd[0],c_str,sizeof(c_str)/sizeof(c_str[0]));//由于不知道读多少,所以读取最大长度
        if(n<0)
        {
            if(errno==EAGAIN)
            {
                write(STDOUT_FILENO,"try again...\n",13);
                sleep(1);
                goto tryagain;
            }
            perror("read");
            exit(1);
        }
        close(fd[0]);
        write(STDOUT_FILENO,c_str,n);
    }
    else//parents
    {
        char str[]="hello pipe!\n";
        sleep(2);
        close(fd[0]);//关闭读端口
        write(fd[1],str,strlen(str));
        close(fd[1]);
        wait(NULL);//等待回收子进程资源
    }
    return 0;
}

此时,read已经不再是阻塞了。需要注意的是,使用管道技术,应该在fork之前创建管道。

 

2.FIFO

 FIOF也被称为命名管道。未命名的管道pipe只能在两个有共同祖先的进程之间使用。但是通过FIFO,完全不相关的进程也能交换数据。

 

 

 

分别创建只读和只写文件fifo_r.c和fifo_r.c:

/*只读:fifo_r.c*/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
void sys_err(const char *str, int exitno)
{
    perror(str);
    exit(exitno);
}

int main(int argc, char *argv[])
{
    int fd, len;
    char buf[1024];
    if (argc < 2) {
        printf("usage:%s fifoname\n",argv[0]);
        exit(1);
    }
    if(access(argv[1],F_OK)==-1)
    {
        if(mkfifo(argv[1],0775)==-1)
        {
            sys_err("mkfifo",1);
        }
    }
     printf("1\n");
    fd = open(argv[1], O_RDONLY);
    if (fd < 0) 
        sys_err("open", 1);
     printf("2\n");
    len = read(fd, buf, sizeof(buf));
    write(STDOUT_FILENO, buf, len);

    close(fd);

    return 0;
}

 

/*只写:fifo_w.c*/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>

void sys_err(const char *str, int exitno)
{
    perror(str);
    exit(exitno);
}

int main(int argc, char *argv[])
{
    int fd;
    char buf[1024] = "hello nanmed pipe!\n";
    if (argc < 2) {
        printf("usage:%s fifoname\n",argv[0]);
        exit(1);
    }
    if(access(argv[1],F_OK)==-1)
    {
        if(mkfifo(argv[1],0775)==-1)
        {
            sys_err("mkfifo",1);
        }
    }
    printf("1\n");
    fd = open(argv[1], O_WRONLY);
    if (fd < 0) 
        sys_err("open", 1);
     printf("2\n");
    write(fd, buf, strlen(buf));
    close(fd);

    return 0;
}

 

先运行写进程,此时程序阻塞在只写打开的open函数,再ctrl+shift+n,打开新的终端,运行只读进程:

 

此时读进程正确读取了写进程的数据。反之,先执行读取进程,读进程也会阻塞在只读open函数处,直到写入数据。前提是没有指定O_NONBLOCK标志。

FIFO和PIPE的最大数据量可以通过fpathconf函数得到:

 在创建了FIFO或者PIPE之后使用:printf("FIFO_PIPE_BUF_SIZE = %ld\n",fpathconf(fd, _PC_PIPE_BUF));

 

 

可以发现,下ubuntu 16.04中,FIFO和PIPE的缓冲区大小为4096个字节。不同的系统版本,可能存在差异。

 

3.内存共享映射

 

 

 sysconf(_SC_PAGESIZE)的返回值,在本文的ubuntu16.04中为4096字节。故off的值应该是4096的整数倍,通常该值设置为0。

现在,使用mmap实现一个复制指令:

 

 1 #include <stdio.h>
 2 #include <pthread.h>
 3 #include <signal.h>
 4 #include <stdlib.h>
 5 #include <unistd.h>
 6 #include <sys/time.h>
 7 #include <sys/resource.h>
 8 #include <sys/types.h>
 9 #include <sys/stat.h>
10 #include <fcntl.h>
11 #include <syslog.h>
12 #include <string.h>
13 #include <sys/mman.h>
14  
15  
16 #define COPYINCR (1024*1024*1024) /* 1 GB */
17 int main(int argc, char *argv[])
18 {
19     int fdin, fdout;
20     void *src, *dst;
21     size_t copysz;
22     struct stat sbuf;
23     off_t fsz = 0;
24     if (argc != 3)
25         printf("usage: %s <fromfile> <tofile>", argv[0]);
26     if ((fdin = open(argv[1], O_RDONLY)) < 0)
27         printf("can’t open %s for reading", argv[1]);
28     if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0666)) < 0)
29         printf("can’t creat %s for writing", argv[2]);
30     if (fstat(fdin, &sbuf) < 0) /* need size of input file */
31         printf("fstat error");
32     if (ftruncate(fdout, sbuf.st_size) < 0) /* 文件字节数:sbuf.st_size ,set output file size */
33         printf("ftruncate error");
34     
35     if ((sbuf.st_size - fsz) > COPYINCR)
36             copysz = COPYINCR;
37     else
38             copysz = sbuf.st_size - fsz;
39     if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED, fdin, fsz)) == MAP_FAILED)
40             printf("mmap error for input");
41     if ((dst = mmap(0, copysz, PROT_READ | PROT_WRITE,MAP_SHARED, fdout, fsz)) == MAP_FAILED)
42             printf("mmap error for output");
43     
44     memcpy(dst, src, copysz); /* does the file copy */
45     
46     munmap(src, copysz);//释放内存
47     munmap(dst, copysz);//释放内存
48         
49     
50     exit(0);
51 }

 

 

使用Vim打开对比,内容自然也是完全一致的:

 这个例子相当于在磁盘的main.c映射一个地址空间到src(只读),然后创建另一个文件,可读可写,通过前面映射的只读地址空间,将其内容拷贝到此时创建的main.c.copy中。

这个思想可以应用在多进程的通信中。本文目前描述的情况,都是最简单的场景,不存在进程间的竞争关系,如多个进程同时写一个文件,此时则需要执行相应的处理方法,如信号量,互斥锁等,这个在后面的随笔中再介绍。

 消息邮箱和socket的进程间通信方法,也将在后续随笔中介绍。

posted @ 2019-05-01 11:17  Crystal_Guang  阅读(1026)  评论(3编辑  收藏  举报