首页 > 技术文章 > linux学习之进程篇(三)

rainbow1122 2017-11-10 22:32 原文

进程之间的通信

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

进程间通信

1.pipe管道

可以用环形队列实现。队列满的话会阻塞。管道是一种最基本的IPC机制,由pipe函数创建

#include<unistd.h>

int pipe(int filedes[2]);

管道作用于有血缘关系的进程之间,通过fork来传递。

调用pipe后,父进程创建管道,fd[1]管道写端,fd[0]管道读端,都是文件描述符,描述符分配是未被使用的最小单元,若最小未被使用的文件描述符是3,则3记录管道的读端,4记录管道的写端,总的来说读端的文件描述符较小,写端的文件描述符较大。父进程fork出子进程,上图中的左边是父进程,右边是子进程。子进程会进程父进程的文件描述表,3仍然指向管道的读端,4指向写端。创建好管道后,要确定好通信方向,有父写子读(关闭父读,关闭子写)和子写父读(关闭子读,关闭父写)两种选择,是单工方式工作。若需要双向通信,需要创建管道,仍是先创建管道,后fork。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
int main(void)
{
    int fd[2];
    char str[1024] = "hello itcast";
    char buf[1024];
    pid_t pid;
    //fd[0] 读端
    //fd[1] 写端
    if (pipe(fd) < 0) {
        perror("pipe");
        exit(1);
    }
    pid = fork();
    //父写子读
    if (pid > 0) {
        //父进程里,关闭父读
        close(fd[0]);
        sleep(5);
        write(fd[1], str, strlen(str));
        close(fd[1]);
        wait(NULL);
    }
    else if (pid == 0) {
        int len, flags;
        //子进程里,关闭子写
        close(fd[1]);

        flags = fcntl(fd[0], F_GETFL);
        flags |= O_NONBLOCK;
        fcntl(fd[0], F_SETFL, flags);
tryagain:
        len = read(fd[0], buf, sizeof(buf));
        if (len == -1) {
            if (errno == EAGAIN) {
                write(STDOUT_FILENO, "try again\n", 10);
                sleep(1);
                goto tryagain;
            }
            else {
                perror("read");
                exit(1);
            }
        }
        write(STDOUT_FILENO, buf, len);
        close(fd[0]);
    }
    else {
        perror("fork");
        exit(1);
    }
    return 0;
}

运行结果:

使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志): 
(1) 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。 
(2)如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。 
(3) 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止
(4) 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

简而言之:写关闭,读端读取管道里内容时,再次读,返回0,相当于读到EOF;写端未关闭,写端暂时无数据,读端读完管道里数据时,再次读会阻塞;读端关闭,写端写管道,产生SIGPIPE信号,写进程默认情况下会终止进程;读端未读管道数据,当写端写满管道后,再次写,阻塞。

管道的这四种特殊情况具有普遍意义。

非阻塞管道,fcntl函数设置O_NONBLOCK标志

fpathconf(int fd,int name)测试管道缓冲区大小,_PC_PIPE_BUF。

2.fifo有名管道

创建一个有名管道,解决无血缘关系的进程通信,fifo:

fifo是一个索引节点,不会再磁盘下留下任何大小,所以没有myfifo的大小为0;

//写管道
#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(char *str, int exitno) { perror(str); exit(exitno); } int main(int argc, char *argv[]) { int fd; char buf[1024] = "hello xwp\n"; if (argc < 2) { printf("./a.out fifoname\n"); exit(1); } //fd = open(argv[1], O_RDONLY); fd = open(argv[1], O_WRONLY); if (fd < 0) sys_err("open", 1); write(fd, buf, strlen(buf)); close(fd); return 0; }
//读管道
#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(char *str, int exitno)
{
    perror(str);
    exit(exitno);
}

int main(int argc, char *argv[])
{
    int fd, len;
    char buf[1024];
    if (argc < 2) {
        printf("./a.out fifoname\n");
        exit(1);
    }

    fd = open(argv[1], O_RDONLY);
    if (fd < 0) 
        sys_err("open", 1);

    len = read(fd, buf, sizeof(buf));
    write(STDOUT_FILENO, buf, len);

    close(fd);

    return 0;
}

gcc fifo_w.c -o fifo_w

gcc fifo_r.c  -o fifo_r

./fifo_w myfifo

./fifo_r myfifo

//函数形式,在编程中使用
#include<sys/types.h> #include<sys/stat.h> int mkfifo(const char *pathname,mode_t mode);

3.内存共享映射

mmap/munmap

 mmap可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就对应内存地址,对文件的读写可以直接用指针而不需要read/write函数。

用于进程间通信时,一般设计成结构体,来传输通信的数据;进程间通信的文件,应该设计成临时文件(根据需求灵活设计);当报总线错误时,优先查看共享文件是否有存储空间(不可以为大小0的磁盘文件)

                                                                                                    mmap

#include<sys/mman.h>

void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offsize); 
具体参数含义
addr :  指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length:  代表将文件中多大的部分映射到内存。
prot  :  映射区域(内存)的保护方式。可以为以下几种方式的组合:
                    PROT_EXEC 映射区域可被执行
                    PROT_READ 映射区域可被读取
                    PROT_WRITE 映射区域可被写入
                    PROT_NONE 映射区域不能存取
flags :  影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
                    MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
                    MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
                    MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
                    MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
                    MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
                    MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
fd    :  要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,
          然后对该文件进行映射,可以同样达到匿名内存映射的效果。
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是PAGE_SIZE(页面大小)的整数倍。

返回值:
      若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。

错误代码:
            EBADF  参数fd 不是有效的文件描述词
            EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
            EINVAL 参数start、length 或offset有一个不合法。
            EAGAIN 文件被锁住,或是有太多内存被锁住。
            ENOMEM 内存不足。
用户层的调用很简单,其具体功能就是直接将物理内存直接映射到用户虚拟内存,使用户空间可以直接对物理空间操作。但是对于内核层而言,其具体实现比较复杂。

#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdlib.h>
int main(void)
{
    int fd, len;
    int *p;
    fd = open("hello", O_RDWR);
    if (fd < 0) {
        perror("open");
        exit(1);
    }
    len = lseek(fd, 0, SEEK_END);

    p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED) {
        perror("mmap");
        exit(1);
    }

    close(fd);
//映射并没有解除
p[0] = 0x30313233; munmap(p, len); return 0; }

修改磁盘文件时,对应映射的内存也会改变。缓输出机制。

mmap的实现原理

//mmap_w
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define MAPLEN  0x1000

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

void sys_err(char *str, int exitno)
{
    perror(str);
    exit(exitno);
}
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_RDWR | O_CREAT, 0777);
    if (fd < 0)
        sys_err("open", 1);

    if (lseek(fd, MAPLEN-1, SEEK_SET) < 0)//扩展
        sys_err("lseek", 3);

    if (write(fd, "\0", 1) < 0)
        sys_err("write", 4);

    mm = mmap(NULL, MAPLEN, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (mm == MAP_FAILED)
        sys_err("mmap", 2);

    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;
}
//mmap_r
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define MAPLEN  0x1000

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

void sys_err(char *str, int exitno)
{
    perror(str);
    exit(exitno);
}
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_RDWR);
    if (fd < 0)
        sys_err("open", 1);

    mm = mmap(NULL, MAPLEN, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (mm == MAP_FAILED)
        sys_err("mmap", 2);

    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;
}

 4.Unix Domain Socket

学习网络编程socket时再来介绍此方法。

例题:利用fifo实现本地聊天室

本地聊天室

  登录时,和服务器建立了一个私有管道,服务器向客户端写,本来有一条从客户端到服务端的公共管道,客户端想服务端写。服务端可以根据解析协议号,做对应的处理。客户端需要检测读标准输入,写标准输出,有四个接口。读标准输入要设为非阻塞(fcntl),非阻塞读私有管道。这要A没有和别人聊天时,还是可以接受别人发过的内容。server阻塞读。(阻塞和非阻塞是针对同一个进程而言)

推荐阅读