zoukankan      html  css  js  c++  java
  • linux学习之进程篇(三)

    进程之间的通信

      每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进行之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程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
    ", 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 "; if (argc < 2) { printf("./a.out fifoname "); 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
    ");
            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
    ");
            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, "", 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
    ");
            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
    ", mm->id);
            printf("%s
    ", mm->name);
            printf("%c
    ", mm->sex);
            sleep(1);
        }
        munmap(mm, MAPLEN);
        return 0;
    }

     4.Unix Domain Socket

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

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

    本地聊天室

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

  • 相关阅读:
    python json 和 pickle的补充 hashlib configparser logging
    go 流程语句 if goto for swich
    go array slice map make new操作
    go 基础
    块级元素 行内元素 空元素
    咽炎就医用药(慢性肥厚性咽炎)
    春季感冒是风寒还是风热(转的文章)
    秋季感冒 咳嗽 怎么选药
    解决IE浏览器“无法显示此网页”的问题
    常用的 css 样式 记录
  • 原文地址:https://www.cnblogs.com/rainbow1122/p/7816773.html
Copyright © 2011-2022 走看看