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阻塞读。(阻塞和非阻塞是针对同一个进程而言)

  • 相关阅读:
    做足以让自己骄傲的活
    Count(*) 与 count(field) 一样吗?
    Explain Plan试分析
    Oracle SQL Developer中查看解释计划Explain Plan的两种方法
    整理+学习《骆昊-Java面试题全集(上)》
    【转】Java就业指导
    如何清晰的、高质量的给面试官介绍自己的电商项目【借鉴】
    留存的图片
    Linux学习_006_JavaEE程序员常用linux命令整理
    给Linux初学者的七个建议,值得一读
  • 原文地址:https://www.cnblogs.com/rainbow1122/p/7816773.html
Copyright © 2011-2022 走看看