4.进程间通信

 

一.实现原理

首先利用了进程的一个共性,即:用户空间不共用,内核空间共用

每个进程各自有不同的用户地址空间, 任何一个进程的全局变量在另一个进程中都看不到,所有进程之间要交换数据必须通过内核,因此可以在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信管道就是内核当中的一块缓冲区

Linux实现进程间通信主要有四种途径:pipe管道,fifo有名管道,内存共享映射,以及Socket。

二.pipe管道

由pipe()函数创建:

#include <unistd.h>
int pipe(int filedes[2]);

其主要处理有血缘关系的进程间通信(即:通过fork来创建的进程)。

实现原理:当调用pipe函数后,内核当中会创建一条管道(环形队列),并存在两个文件描述符,而通过fork()函数创建的子进程可以继承PCB和文件描述符表,因此可以拿到pipe管道对应的两个文件描述符,可通过这两个文件描述符实现进程间通信。

使用pipe管道需要注意:

两个进程通过一个管道只能实现单向通信。因此创建好管道后,需要确立通信方向。即:究竟是父写子读,还是子写父读。确立单工的通信模式可以规避诸如此类的问题:假设父进程通过管道写了一段内容,子进程通过管道也写了一段内容,回头分不清楚,这段内容究竟是谁写的?若要实现双向通信,则必须使用两个管道。
例1:

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

int main()
{

    int fd[2];
    pid_t pid;
    char str[] = "This is a new question\n";
    char buf[1024];    

    // 父写子读
    if (pipe(fd) < 0) {
        perror("pipe");
    }
        
    pid = fork();
    if (pid > 0) { // parent
        //  关闭父读
        close(fd[0]);
        write(fd[1], str, strlen(str));
        sleep(3);
        // 回收子进程
        wait(NULL);
    } else if (pid == 0) { // child
        // 关闭子写
        close(fd[1]);
        int len = read(fd[0], buf, sizeof(buf));        
        write(STDOUT_FILENO, buf, len);
    } else {
        perror("fork");
    }
    return 0;
}

运行结果:

This is a new question

使用pipe管道须注意:

1.写端关闭,读端读完管道里的内容时,再次读,返回0,相当于读到EOF
2.写端未关闭,写端暂时无数据,读端读完管道里的数据后,再次读,阻塞
3.读端关闭,写端写管道,产生SIGPIPE信号(17),这个信号会导致写进程终止
4.读端未读管道数据,当写端写满管道数据后,再次写,阻塞

例2:改变管道的阻塞属性,使子进程在读管道时不被阻塞

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

int main()
{
    
    int fd[2], flags, len;
    pid_t pid;
    char str[] = "This is not impossibile\n";
    char buf[1024];

    if (pipe(fd) < 0) {
        perror("pipe");
    }
    
    // 设置读管道为非阻塞
    flags = fcntl(fd[0], F_GETFL);    
    flags |= O_NONBLOCK;
    fcntl(fd[0], F_SETFL, flags);
    
    pid = fork();
        
    /*父写子读*/
    if (pid > 0) { // parent
        close(fd[0]);
        sleep(6);
        write(fd[1], str, strlen(str));
        wait(NULL);
    } else if (pid == 0) { // child
        close(fd[1]);
tryagain:
        len = read(fd[0], buf, sizeof(buf));
        if (len == -1) {
            if (errno == EAGAIN) { // 读一个非阻塞文件,如果数据没到达,就会出现EAGAIN
                write(STDOUT_FILENO, "try again\n", 10);
                sleep(1);
                goto tryagain;
            } else {
                perror("read");
                exit(1);
            }    
        }
        write(STDOUT_FILENO, buf, len);
    } else {
        perror("fork");
    }
    
    return 0;
}

运行结果:

try again
try again
try again
try again
try again
try again
This is not impossibile

例3:可以使用fpathconf()函数来获取管道缓冲区的大小

#include <unistd.h>
#include <stdio.h>


int main()
{
    
    int fd[2];
    
    if (pipe(fd) < 0) {
        perror("pipe");
    }
    
    long value = fpathconf(fd[0], _PC_PIPE_BUF);
    printf("The pipe buf is: %ld\n", value);
    close(fd[0]);
    close(fd[1]);
    return 0;
}

运行结果:

The pipe buf is: 4096

三.fifo有名管道

由mkfifo()函数创建管道文件

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

其可以来处理无血缘关系的进程间通信

实现原理:在磁盘当中创建一个文件结点(仅仅是一个文件结点,没有大小),标记内核当中的一个pipe,继而通过操作该文件(open该文件,然后进行读写)实现进程间通信。 实际上也是在内核中创建了一个缓冲区。

使用fifo管道需要注意:

1.当以只写的方式打开FIFO管道时,如果没有FIFO的读端打开,则open写打开会阻塞。

2.FIFO内核实现时可以支持双向通信。(pipe单向通信,因为父子进程共享一个FILE 结构体)

3.FIFO管道可以有一个读端,多个写端,也可以有多个读端,一个写端。

4.mkfifo同时也是一个命令,可直接在终端下使用mkfifo命令创建一个管道文件:

  

例:使用fifo管道实现进程间通信

写端:

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

int main()
{
    int fd;
    char str[] = "This is a new question\n";    
    int rand_value;
    char buf[100];    

    if (access("myfifo", F_OK) < 0) { // File not exists
        int ret = mkfifo("myfifo", 0777);
        if (ret < 0) {
            perror("mkfifo");
            exit(1);
        }
    } else { // file exists
        fd = open("myfifo", O_WRONLY);
        if (fd < 0) {
            perror("open");
            exit(2);
        }
        srand((int)time(0)); /*利用time(0)作为随机数种子*/
        while (1) {
            rand_value = 1 + (int)(10.0*rand() / (RAND_MAX+1.0));
            memset(buf, 0, sizeof(buf));
            sprintf(buf, "current random value is: %d\n", rand_value);
            write(fd, buf, strlen(buf));
            sleep(2);        
        }
        close(fd);
    }
    return 0;
}

读端:

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

int main()
{
    int fd;
    char buf[1024];
    
    if (access("myfifo", F_OK) < 0) {
        perror("access");
        exit(1);    
    } else {
        fd = open("myfifo", O_RDONLY);
        if (fd < 0) {
            perror("open");
            exit(2);
        }
        while (1) {
            int len = read(fd, buf, sizeof(buf));
            if (len < 0) {
                perror("read");
                exit(3);
            } else if (len > 0) {
                write(STDOUT_FILENO, buf, len);
            } 
        }
        close(fd);
    }
        
    return 0;
}

先运行写端,再运行读端:

current random value is: 10
current random value is: 8
current random value is: 3
current random value is: 5
current random value is: 10
current random value is: 10
current random value is: 8
current random value is: 4
current random value is: 8
current random value is: 1
current random value is: 8
current random value is: 5
current random value is: 5
current random value is: 7

四.内存共享映射(mmap()&munmap())

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

内存共享映射同样可以用来处理无血缘关系的进程间通信

实现原理:mmap()函数可以将磁盘文件中的某一部分内容映射到内存,通过设置相关参数还可以实现磁盘文件内容与内存同步,若进程1通过修改内存来更新文件,进程2如果再将该文件映射到内存,当文件内容发生改变后,进程2所映射的内存同样也会发生改变,因此可实现进程间通信,此方法与FIFO类似,仍然需要一个文件作为交互媒介,与FIFO不同的是,将文件内容映射到内存后,对文件的读写可直接使用指针来操作,不再需要使用read()/write()函数。且用来交互的文件不必再是一个管道文件,普通文件亦可,通信结束后,还可以将该文件删除。

参数说明:

addr: 如果addr参数为NULL,内核会自己在进程地址空间中选择合适的地址建立映射。如果addr不是NULL,则给内核一个提示,应该从什么地址开始映射,内核会选择addr之上的某个合适的地址开始映射。建立映射后,真正的映射首地址通过返回值可以得到。

length: 申请内存的长度。

prot:  设置映射内存的访问权限。

flags: 状态标志。指定映射类型,SHARED(共享)/PRIVATED(私有)等
fd:  文件描述符,映射磁盘文件的文件描述符
offset: 你要映射的磁盘文件从什么地址开始偏移。偏移量必须是分页大小的整数倍,偏移的时候是按页面为单位偏移的。

返回值: mmap()函数所申请的内存的首地址。

使用内存共享映射还需要注意:

1.用于进程间通信时,一般设计成结构体,来传输通信的数据

2.进程间通信的文件,应该设计成临时文件

3.当报总线错误时,优先查看共享文件是否有存储空间

例:

写端:

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

#define MAPLEN 0x1000


struct STU {
    int id;
    char name[20];
    char sex;
};




int main(int argc, char *argv[])
{
    struct STU *mm;
    int fd, i = 0;

    if (argc < 2) {
        printf("./a.out filename\n");
        exit(1);
    }
    fd = open(argv[1], O_CREAT|O_RDWR, 0777);    
    if (fd < 0) {
        perror("open");
        exit(2);
    }

    if (lseek(fd, MAPLEN-1, SEEK_SET) < 0) {
        perror("lseek");
        exit(3);
    }

    if (write(fd, "\0", 1) < 0) {
        perror("write");
        exit(4);
    }

    mm = mmap(NULL, MAPLEN, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (mm == MAP_FAILED) {
        perror("mmap");
        exit(5);
    }
    close(fd);

    while (1) {
        mm->id = i;
        sprintf(mm->name, "zhang-%d", i);
        if (i % 2 == 0) {
            mm->sex = 'm';
        } else {
            mm->sex = 'w';
        }
        i++;
        sleep(1);
    }

    munmap(mm, MAPLEN);
    return 0;
}

读端:

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

#define MAPLEN 0x1000

struct STU {
    int id;
    char name[20];
    char sex;
};


int main(int argc, char *argv[])
{

    struct STU *mm;
    int fd;

    if (argc < 2) {
        printf("./a.out filename\n");
        exit(1);
    }
    
    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        perror("open");
        exit(2);
    }    
    
    mm = mmap(NULL, MAPLEN, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (mm == MAP_FAILED) {
        perror("mmap");
        exit(3);
    }

    close(fd);
    unlink(argv[1]);
    while (1) {
        printf("%d\n", mm->id);
        printf("%s\n", mm->name);
        printf("%c\n", mm->sex);
        sleep(1);
    }
    munmap(mm, MAPLEN);
    return 0;
}

先运行写端,再运行读端:

3
zhang-3
w
4
zhang-4
m
5
zhang-5
w
6
zhang-6
m
7
zhang-7
w
8
zhang-8
m
9
zhang-9
w
10
zhang-10
m

 

posted @ 2018-01-03 22:57  夜行过客  阅读(336)  评论(0编辑  收藏  举报