【Linux系统编程】Linux多进程开发-进程间通信

  • Post author:
  • Post category:linux




进程间通信***
  • 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联。不能在一个进程中直接访问另一个进程的资源。
  • 但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信(IPC: Inter Processes Communication)。
  • 进程间通信的目的:

    • 数据传输:一个进程需要将它的数据发送给另一个进程。
    • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
    • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
    • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。


Linux进程间通信的方式

(还有内存映射)



匿名管道
  • 管道也叫无名(匿名)管道,是UNIX系统IPC(进程间通信)的最古老形式,所有的UNIX系统都支持这种通信机制。
  • 统计一个目录中文件的数目命令: ls | wc -l,为了执行该命令, shell 创建了两个进程来分别执行 ls 和 wc。

特点:

  • 管道:一个在

    内核内存

    中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。

  • 管道拥有

    文件的特质



    读操作、写操作

    • 匿名管道:没有文件实体(有关系的进程间:父子进程等)
    • 有名管道:有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。(没有关系的进程间)
  • 一个管道是一个

    字节流

    ,使用管道时不存在

    消息或者消息边界

    的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。

  • 通过管道传递的数据是

    顺序

    的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一致的。

  • 在管道中的数据的传递方向是单向的,一端用于写入,—端用于读取,管道是半双工的。

  • 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃.释放空间以便写更多的数据.在管道中无法使用 lseek() 来随机的访问数据。

  • 匿名管道只能在具有

    公共祖先的进程

    (父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。


为什么可以使用管道进行进程间通信?


我们一开始在父进程中通过fork()创建子进程,父子进程的虚拟地址空间实际上是基于”读时共享(子进程被创建,两个进程没有做任何写的操作),写时拷贝”原则。即子进程内核区中文件描述符表是和父进程保持一致的(在不后期修改的前提下)。那么只要在fork()之前通过pipe()创建匿名管道获取对读端和写端的文件描述符,之后fork()就可以让父子进程同时获得对该管道的读/写权限,那么就可以进行进程间通信。


管道数据结构

环形队列:空间可以重复使用;数据只能被读取一次。


三种匿名管道通信的情况:


匿名管道的使用

#include <unistd.h>
int pipe(int pipefd[2]);
        功能:创建一个匿名管道,用来进程间通信
        参数:
            -int pipefd[2]: 是一个传出参数
                pipefd[0]: 对应的是管道的读取端
                pipefd[1]: 对应的是管道的写入端
        返回值:
            成功返回0;失败返回-1,设置errno。
    管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞。
    注意:匿名管道只能用于具有关系的进程之间的通信(父子进程 兄弟进程等)
	eg.
    	int pipefd[2];
		int ret = pipe(pipefd);

#include <unistd.h>
long fpathconf(int fd, int name);
	eg.
        int pipefd[2];
    	int ret = pipe(pipefd);
    	// 获取管道的大小
    	long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
# 查看管道缓冲大小命令
boyangcao@MyLinux:~$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 15407
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 15407
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited


示例:父子进程分别进行数据的输出与接收(半双工通信)

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

int main()
{
    // 在fork之前创建管道
    int pipefd[2];

    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        exit(0);
    }
    // 创建子进程
    pid_t pid = fork();
    if(pid > 0)
    {
        // 父进程
        printf("i am parent process, pid: %d\n", getpid());
        char buf[1024] = {0}; 
        while(1)
        {
            // 从管道的读取端读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv: %s, pid : %d\n", buf, getpid());

            // 向管道中写入数据
            char * str = "Hello, i am parent";
            write(pipefd[1], str, strlen(str));
            sleep(1);//使得write完成后被另一进程抢夺CPU资源,从而读取管道数据,实现通信
        }       
    }
    else if(pid == 0)
    {
        // 子进程
        printf("i am child process, pid: %d\n", getpid());
        char buf[1024] = {0};
        while(1)
        {
            // 向管道中写入数据
            char * str = "Hello, i am child";
            write(pipefd[1], str, strlen(str));
            sleep(1);//使得write完成后被另一进程抢夺CPU资源,从而读取管道数据,实现通信
            // 从管道的读取端读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("child recv: %s, pid : %d\n", buf, getpid());
        }       
    }
    return 0;
}

注意:在匿名管道中进行相互通信时,有时候会出现一个进程连续进行读和写管道的操作(自己接收自己发出的信息),所以在用匿名管道通信时一般会限定其为单向通信。

即:

// 关闭读端(或者写端)
close(pipefd[0]);


示例:实现 ps aux | grep xxx 父子进程间通信

/*
    实现 ps aux | grep xxx  父子进程间通信
    子进程:ps aux,子进程结束后,将数据发送给父进程
    父进程:获取到数据,过滤
    pipe()
    execlp()
    子进程将标准输出 stdout_fileno 重定向到管道的写端. dup2()
*/
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
int main()
{
    // 创建一个管道
    int fd[2];
    int ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe");
        exit(0);
    }
    // 创建子进程
    pid_t pid = fork();
    if(pid > 0)
    {
        // 父进程
        // 关闭写端
        close(fd[1]);
        // 从管道中读取数据
        char buf[1024] = {0};
        int len = -1;
        while((len = read(fd[0], buf, sizeof(buf) - 1)) > 0)  // 留出一个字符串结束符
        {
            // 过滤数据并输出
            printf("%s", buf);
            memset(buf, 0, 1024); // 清空数组内容
        }
        wait(NULL);
    }
    else if(pid == 0)
    {
        // 子进程
        // 关闭读端
        close(fd[0]);
        // 文件描述符的重定向 stdout_fileno -> fd[1]
        // 执行 ps aux
        dup2(fd[1], STDOUT_FILENO);
        // 执行
        execlp("ps", "ps", "aux", NULL);
        // 执行execlp()失败返回错误
        perror("execlp");
        exit(0);
    }
    else{
        perror("fork");
        exit(0);
    }
    return 0;
}


管道读写特点:

使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作):


  1. 所有

    的指向管道

    写端的文件描述符都关闭了

    (管道写端引用计数为0)

    有进程从管道的读端读数据,

    管道中剩余的数据被读取以后再次read会返回0

    ,就像读到文件末尾一样。

  2. 如果

    有指向管道写端的文件描述符没有关闭

    (管道的写端引用计数大于0),而

    持有管道写端的进程



    也没有往管道中写数据

    此时有进程从管道中读取数据,

    管道中剩余的数据被读取后再次read会阻塞

    ,直到管道中有数据可以读了才读取数据并返回。

  3. 如果

    所有指向管道读端的文件描述符都关闭

    了(管道的读端引用计数为0):

    此时有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE,通常会导致进程异常终止。

  4. 如果

    有指向管道读端的文件描述符没有关闭

    (管道的读端引用计数大于0),而

    持有管道读端的进程



    也没有从管道中读数据

    这时有进程向管道中写数据,那么在

    管道被写满的时候再次write会阻塞

    ,直到管道中有空位置才能再次写入数据并返回。

  • 读管道:

    • 管道中有数据,read返回实际读到的字节数。
    • 管道中无数据:

      • 写端被全部关闭,read返回0(相当于读到文件的末尾EOF)
      • 写端没有完全关闭,read阻塞等待(设置为非阻塞 O_NONBLOCK 则返回-1)
  • 写管道:

    • 管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
    • 管道读端没有全部关闭:

      • 管道已满,write阻塞
      • 管道没有满,write将数据写入,并返回实际写入的字节数

示例:设置管道读端非阻塞

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
/*
    设置管道非阻塞
    int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
    flags |= O_NONBLOCK;             // 修改flag的值
    fcntl(fd[0], F_SETFL, flags); // 设置新的flag
*/
int main()
{
    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        exit(0);
    }
    // 创建子进程
    pid_t pid = fork();
    if(pid > 0)
    {
        // 父进程
        printf("i am parent process, pid: %d\n", getpid());
        // 关闭写端
        close(pipefd[1]);
        // 设置管道非阻塞
        int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag
        flags |= O_NONBLOCK;             // 修改flag的值
        fcntl(pipefd[0], F_SETFL, flags); // 设置新的flag

        char buf[1024] = {0}; 
        while(1)
        {
            // 从管道的读取端读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len: %d\n", len);
            printf("parent recv: %s, pid : %d\n", buf, getpid());
            memset(buf, 0, 1024); // 清空数组内容 将数组buf中1024B设置为0
            sleep(1);
        }       
    }
    else if(pid == 0)
    {
        // 子进程
        printf("i am child process, pid: %d\n", getpid());
        char buf[1024] = {0};
        // 关闭读端
        close(pipefd[0]);
        while(1)
        {
            // 向管道中写入数据
            char * str = "Hello, i am child";
            write(pipefd[1], str, strlen(str));
            sleep(5);
        }       
    }
    return 0;
}


有名管道
  • 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。

  • 有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO相互通信,因此,通过FIFO 不相关的进程也能交换数据。

  • 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了(如read ()、 write()和close())。

    与管道一样, FIFO也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO的名称也由此而来:先入先出。

有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于:

  1. FIFO在文件系统中作为一个特殊文件存在(有文件实体,而匿名管道没有),不过FIFO文件为空, FIFO 中的内容都存放在内存(内核缓冲区)中。
  2. 当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。
  3. FIFO 有名字,不相关的进程(有关系的进程也可以进行通信)可以通过打开有名管道进行通信。
创建FIFO文件
    1. 通过命令: mkfifo 名字
    2. 通过函数: int mkfifo(const char *pathname, mode_t mode);

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo (const char *pathname, mode_t mode);
        参数:
            - pathname: 管道名称的路径 (相对/绝对路径)
            - mode: 文件权限 和open的mode一致 是一个八进制数
        返回值:成功返回0;失败返回-1,设置errno
        eg. int ret = mkfifo("fifo1", 0664);
  • 使用mkfifo创建一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于FIFO。如: close、read、write、unlink等。

  • FIFO严格遵循先进先出 (First in First out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek ( )等文件定位操作。


    有名管道的注意事项:

  1. ​ 一个为 只读 而打开一个管道的进程会阻塞,直到另外一个进程为 只写 打开管道
  2. ​ 一个为 只写 而打开一个管道的进程会阻塞,直到另外一个进程为 只读 打开管道
  • 读管道:

    • 管道中有数据,read返回实际读到的字节数
    • 管道中无数据:

      • 管道写端被全部关闭,read返回0,(相当于读到文件末尾)
      • 写端没有全部被关闭,read阻塞等待。
  • 写管道:

    • 管道读端被全部关闭,进程会异常终止(收到一个SIGPIPE信号)
    • 管道读端没有被全部关闭:

      • 管道已经满了,write会阻塞
      • 管道没有满,write将数据写入,返回实际写入的字节数。

示例:write.c 向管道写数据 read.c 从管道读数据

// write.c
// 向管道中写数据
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
    // 1. 判断管道文件是否存在
    int res = access("test", F_OK);
    if(res == -1)
    {
        printf("管道不存在,创建管道\n");
        // 2. 创建管道文件
        int ret = mkfifo("test", 0664);

        if(ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    // 3. 以只写的方式打开管道
    int fd = open("test", O_WRONLY);
    if(fd == -1)
    {
        perror("open");
        exit(0);
    }

    // 4. 写数据
    for(int i = 0; i < 100; i++)
    {
        char buf[1024];
        sprintf(buf, "Hello, %d\n", i);
        printf("write data: %s\n", buf);
        write(fd, buf, strlen(buf));
        sleep(1);
    }
    
    close(fd);
    return 0;
}

// read.c
// 从管道中读数据
int main()
{
    // 1. 打开管道文件
    int fd = open("test", O_RDONLY);
    if(fd == -1)
    {
        perror("open");
        exit(0);
    }

    // 读数据
    while(1)
    {
        char buf[1024] = {0};
        int len = read(fd, buf, sizeof(buf));
        if(len == 0)
        {
            printf("写端断开连接...\n");
            break;
        }
        printf("recv buf: %s\n", buf);
    }
    close(fd);
    return 0;
}


案例:用有名管道实现聊天功能

在这里插入图片描述

但是上述只能实现“你说一句我回答一句”的效果,无法连续发送或接收多条信息。

解决方案:将写入管道和读取管道分为两个进程并发执行:

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

int main()
{
    // 1. 判断管道文件是否存在
    int ret = access("fifo1",F_OK);
    if(ret == -1)
    {
        // 文件不存在
        printf("管道文件不存在,创建对应的管道文件\n");
        ret = mkfifo("fifo1", 0664);
        if(ret == -1)
        {
            perror("mkfifo");
            exit(-1);
        }
    }
    
    ret = access("fifo2",F_OK);
    if(ret == -1)
    {
        // 文件不存在
        printf("管道文件不存在,创建对应的管道文件\n");
        ret = mkfifo("fifo2", 0664);
        if(ret == -1)
        {
            perror("mkfifo");
            exit(-1);
        }
    }

    // 2. 开辟双进程
    // 并发地写读数据
    char buf[128];
    pid_t pid = fork();
    if(pid > 0)
    {
        // 是父进程:执行写操作
        // 3. 以只写的方式打开fifo1
        int fdw = open("fifo1", O_WRONLY);
        if(fdw == -1)
        {
            perror("open");
            exit(-1);
        }
        printf("打开fifo1成功, 等待写入数据...\n");

        while(1)
        {
            memset(buf, 0, 128); // 将buf内容清空
            // 获取标准输入的数据
            fgets(buf, 128, stdin);
            // 4. 写数据
            ret = write(fdw, buf, strlen(buf));
            if(ret == -1)
            {
                perror("write");
                break;
            }
        }
        // 5. 关闭文件描述符
        close(fdw);
    }
    else if(pid == 0)
    {
        // 是子进程:执行读操作
        // 3. 以只读的方式打开fifo2
        int fdr = open("fifo2", O_RDONLY);
        if(fdr == -1)
        {
            perror("open");
            exit(-1);
        }
        printf("打开fifo2成功, 等待读取数据...\n");
        
        while(1)
        {
            // 4. 读取管道数据
            memset(buf, 0, 128);
            ret = read(fdr, buf, 128);
            if(ret <= 0)
            {
                perror("read");
                break;
            }
            printf("B: %s\n",buf);
        }
        // 5. 关闭文件描述符
        close(fdr);
    }    
    return 0;
}
// chatB.c
#include <stdio.h>
#include <unistd.h>  
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    // 1. 判断管道文件是否存在
    int ret = access("fifo1",F_OK);
    if(ret == -1)
    {
        // 文件不存在
        printf("管道文件不存在,创建对应的管道文件\n");
        ret = mkfifo("fifo1", 0664);
        if(ret == -1)
        {
            perror("mkfifo");
            exit(-1);
        }
    }
    
    ret = access("fifo2",F_OK);
    if(ret == -1)
    {
        // 文件不存在
        printf("管道文件不存在,创建对应的管道文件\n");
        ret = mkfifo("fifo2", 0664);
        if(ret == -1)
        {
            perror("mkfifo");
            exit(-1);
        }
    }

    // 2. 开辟双进程
    // 并发地写读数据
    char buf[128];
    pid_t pid = fork();
    if(pid > 0)
    {
        // 是父进程:执行读操作
        // 3. 以只读的方式打开fifo1
        int fdr = open("fifo1", O_RDONLY);
        if(fdr == -1)
        {
            perror("open");
            exit(-1);
        }
        printf("打开fifo1成功, 等待读取数据...\n");

        while(1)
        {
            // 4. 读取管道数据
            memset(buf, 0, 128);
            ret = read(fdr, buf, 128);
            if(ret <= 0)
            {
                perror("read");
                break;
            }
            printf("A: %s\n",buf);
        }
        // 5. 关闭文件描述符
        close(fdr);
    }
    else if(pid == 0)
    {
        // 是子进程:执行写操作
        // 3. 以只写的方式打开fifo2
        int fdw = open("fifo2", O_WRONLY);
        if(fdw == -1)
        {
            perror("open");
            exit(-1);
        }
        printf("打开fifo2成功, 等待写入数据...\n");
        while(1)
        {
            memset(buf, 0, 128); // 将buf内容清空
            // 获取标准输入的数据
            fgets(buf, 128, stdin);
            // 4. 写数据
            ret = write(fdw, buf, strlen(buf));
            if(ret == -1)
            {
                perror("write");
                break;
            }
        }
        // 5. 关闭文件描述符
        close(fdw);
    }    
    return 0;
}


内存映射

内存映射(Memory-mapped I/O):将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。

  • 效率较高(直接对内存进行操作)
  • 可以实现有关系的进程通信,也可以实现没有关系的进程通信

#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);

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
        功能:将一个文件或者设备数据映射到内存中
        参数:
            - void* addr:  (映射的起始地址由addr指定)
                - NULL, 由内核选择一个地址创建映射 
            - length: 要映射的数据长度,这个值不能为0,建议使用文件长度。
                获取文件的长度:stat() lseek()
                实际数据长度为分页大小的整数倍(不足自动补全)
            - prot: 对申请的内存映射区的操作权限
                -PROT_EXEC  Pages may be executed.      可执行权限
                -PROT_READ  Pages may be read.          读的权限
                -PROT_WRITE Pages may be written.       写的权限
                -PROT_NONE  Pages may not be accessed.  没有权限
                要操作映射内存,必须要有读的权限。
                PROT_READ 、 PROT_READ | PROT_WRITE
            - flags: 
                -MAP_SHARED: 映射区的数据会自动和磁盘文件进行同步,进程间通信必须设置此选项
                -MAP_PRIVATE: 不同步,内存映射区的数据改变,对原来的文件不会修改,会重新创建一个新的文件(copy on write, 写时复制)
                -MAP_ANONYMOUS: The mapping is not backed by any file; its contents are initialized to zero.  The fd argument is ignored;(匿名映射)
            - fd: 需要映射的文件的文件描述符
                - 通过open()得到,open的是一个磁盘文件
                - 注意:
                    - 文件的大小不能为0
                    - open指定的权限不能和prot有冲突 (prot权限一定不能大于open权限)
                        prot: PROT_READ              open: 只读/读写
                        prot: PROT_READ | PROT_WRITE open: 读写
                    - -1: when  MAP_ANONYMOUS  (or MAP_ANON) is specified(匿名映射)
            - offset: 映射偏移量,一般不使用 (必须指定4k的整数倍) 0表示不偏移
                		当为匿名映射时设置为 0
        返回值:返回创建内存的首地址
            -失败返回 MAP_FAILED 即 (void *) -1 并设置errno

int munmap(void *addr, size_t length);
        功能:释放内存映射
        参数:
            - addr: 要释放的内存首地址
            - length: 要释放的内存大小 和mmap()中的length大小一致
        返回值:
            - success, returns 0.  
            - failure, returns -1, and errno is set to indicate the cause of the error (probably to EINVAL).

案例:实现有关系的进程(父子进程)间通信

/*
    使用内存映射实现进程间通信
    1. 有关系的进程(父子进程)
        - 还没有子进程的时候
            - 通过唯一的父进程,先创建内存映射区
        - 有了内存映射区后创建子进程
        - 父子进程共享创建的内存映射区
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    // 打开一个文件
    int fd = open("text.txt", O_RDWR);
    // 获取文件大小
    int size = lseek(fd, 0, SEEK_END);
    // 2. 创建内存映射区
    void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }
    // 3. 创建子进程
    pid_t pid = fork();
    if(pid > 0)
    {
        wait(NULL);
        // 父进程
        char buf[64];
        strcpy(buf, (char*) ptr);
        printf("read data: %s\n", buf);
    }
    else if(pid == 0){
        // 子进程
        strcpy((char *)ptr, "nihao a, son!!!");
    }
    // 关闭内存映射区
    munmap(ptr, size);
    return 0;
}

案例:实现没有关系的进程(父子进程)间通信

/*
    2. 没有关系的进程间通信
        - 准备一个大小不是0的磁盘文件
        - 进程1 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 进程2 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 使用内存映射区进行通信
    注意:内存映射区通信,是非阻塞。
*/

// mmap-norelationship-ipc1.c
int main()
{
    // 打开一个文件
    int fd = open("test.txt", O_RDWR);
    // 获取文件大小
    int size = lseek(fd, 0, SEEK_END);
    // 2. 创建内存映射区
    void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }
    // 读取内存中数据
    char buf[64];
    while(1)
    {
        strcpy(buf, (char*) ptr);
        printf("read data: %s\n", buf);
        sleep(1);
        memset(buf, 0, 64);
    }
    // 关闭内存映射区
    munmap(ptr, size);
    return 0;
}

// mmap-norelationship-ipc2.c
int main()
{
    // 打开一个文件
    int fd = open("test.txt", O_RDWR);
    // 获取文件大小
    int size = lseek(fd, 0, SEEK_END);
    // 2. 创建内存映射区
    void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }
    // 3. 向内存映射区写数据
    char buf[64];
    while(1)
    {
        fgets(buf, 128, stdin);
        strcpy((char *)ptr, buf);
    }
    // 关闭内存映射区
    munmap(ptr, size);
    return 0;
}


思考:
  • 如果对mmap的返回值(ptr)做++操作(ptr++),munmap是否能够成功?

    void * ptr = mmap(...);
    ptr++;  // 可以对其进行++操作,但是不建议
    munmap(ptr, len);   // 错误,要保存地址
    
  • 如果open时 O_RDONLY,mmap 时 prot 参数指定 PROT_READ | PROT_WRITE 会怎样?

    • 错误,返回MAP_FAILED (void*) -1

      open() 函数中的权限建议和 prot 参数的权限保持一致(或者大于 prot 权限)。
  • 如果文件偏移量(offset)为1000会怎样?

    偏移量必须是 4K 的整数倍,返回 MAP_FAILED

  • mmap 还有什么情况下会调用失败?

    • 第二个参数:length = 0

    • 第三个参数:

      • prot 只指定了写权限;

      • prot PROT_READ | PROT_WRITE

        而第5个参数 fd 通过open函数时指定的 O_RDONLY 或者是 O_WRONLY

  • 可以open的时候 O_CREAT 一个新文件来创建映射区吗?

    – 可以的,但是创建的文件的大小如果为0的话,肯定不行

    – 可以对新的文件进行扩展

    – lseek()

    – truncate()

  • mmap后关闭文件描述符,对mmap映射有没有影响?

        int fd = open("XXX");
        mmap(,,,,fd,0);
        close(fd); 
        // 映射区还存在,创建映射区的fd被关闭,没有任何影响。
    	// mmap()对传入进来的文件描述符fd进行了拷贝(dup() / fcntl())
    
  • 对ptr越界操作会怎样?

    void * ptr = mmap(NULL, 100,,,,,);
    // 并不是真的只分配了100B的内存用于内存映射,实际按照分页的大小(4K)进行内存分配
    // 不同系统分页大小不同
    // 越界操作操作的是非法的内存(野内存) -> 段错误
    


文件复制:
// 使用内存映射实现文件拷贝的功能
/*
    思路:
        1. 对原始的文件进行内存映射
        2. 创建一个新文件(拓展该文件)
        3. 把新文件的数据映射到内存中
        4. 通过内存拷贝将第一个文件的内容拷贝新的文件内存中
        5. 释放资源
*/
int main()
{
    // 1.对原始文件进行内存映射
    int fd = open("english.txt", O_RDWR);
    if(fd == -1)
    {
        perror("open");
        exit(-1);
    }

    // 获取原始文件的大小
    int len = lseek(fd, 0, SEEK_END);

    // 2. 创建一个新文件(拓展该文件)
    int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
    if(fd1 == -1)
    {
        perror("open");
        exit(-1);
    }
    // 对新创建的文件进行拓展
    truncate("cpy.txt", len);
    write(fd1, " ", 1);

    // 3. 分别做内存映射
    void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    void* ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);

    if(ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }
    if(ptr1 == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }

    // 内存拷贝
    memcpy(ptr1, ptr, len);
    // 释放资源
    munmap(ptr1,len);
    munmap(ptr,len);

    close(fd1);
    close(fd);
    return 0;
}


匿名映射
/*
    匿名映射:    
        不需要文件实体进行一个内存映射
        只能进行父子进程这些有关系进程的映射
*/
int main()
{
    // 1. 创建匿名内存映射区
    int len = 4096;
    void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(ptr == MAP_FAILED)
    {
        perror("mmap");
        exit(-1);
    }
    // 父子进程间通信
    pid_t pid = fork();
    if(pid > 0)
    {
        // 父进程
        strcpy((char*) ptr, "Hello, World");
        wait(NULL);
    }
    else if (pid == 0)
    {
        // 子进程
        sleep(1);
        printf("%s\n", (char *)ptr);
    }
    // 释放内存映射区
    int ret = munmap(ptr, len);
    if(ret == -1)
    {
        perror("munmap");
        exit(-1);
    }
    return 0;
}


信号

信号是Linux进程间通信的最古老的方式之一、是事件发生时对进程的通知机制,有时也称之为软件中断。它是在软件层次上对中断机制的一种模拟。是一种

异步通信

的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

tips. 硬件中断

发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:

  • 前台进程:用户可以通过输入特殊的终端字符来给它发送信号。比如输入 Ctrl+C 通常会给进程发送一个中断信号(9号信号)。
  • 硬件发生异常:即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除;或者引用了无法访问的内存区域。
  • 系统状态变化:比如alarm定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
  • 运行 kill 命令或调用 kill 函数。

使用信号的两个主要目的是:

  • 让进程知道已经发生了一个特定的事情。
  • 强迫进程执行它自己代码中的信号处理程序。

信号的特点:

  • 简单
  • 不能携带大量信息
  • 满足某个特定条件才发送
  • 优先级比较高

查看系统定义的信号列表: kill -l

# 前31个信号为常规信号,其余为实时信号。(重点掌握前31个信号,后面是预定义好的信号)
boyangcao@MyLinux:~$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	


Linux信号一览表

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-airOpMuG-1659520803018)(D:\SoftWare\typora\Typora_data\image-20220727170808615.png)]

在这里插入图片描述

在这里插入图片描述



信号的5种默认处理动作
  • 查看信号的详细信息: man 7 signal
  • 信号的5种默认处理动作:
Term	# 终止进程
Ign		# 当前进程忽略掉这个信号
core	# 终止进程并生成一个core文件(core: 保存进程异常退出的错误信息,便于进行调试)
stop	# 暂停当前进程
Cont	# 继续执行当前被暂停的进程
  • 信号的几种状态:产生、未决、递达
  • SIGKILL和SIGSTOP信号不能被捕捉、阻塞或者忽略,只能执行默认动作。
# 输出core文件方法:设置core file size 为1024
boyangcao@MyLinux:~/Linux/Lesson26$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 15407
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1048576
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 15407
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
boyangcao@MyLinux:~/Linux/Lesson26$ ulimit -c 1024

注意:有时候指定 gcc core.c -g,再 ./a.out,不会生成 core 文件。

解决办法:使用两种方式不产生core文件是文件生产目录错误

# 查看core文件生产目录
cat /proc/sys/kernel/core_pattern 
# 使用下面的命令
sudo bash -c "echo core > /proc/sys/kernel/core_pattern"

boyangcao@MyLinux:~/Linux/Lesson26$ cat /proc/sys/kernel/core_pattern 
|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E
boyangcao@MyLinux:~/Linux/Lesson26$ sudo bash -c "echo core > /proc/sys/kernel/core_pattern"
boyangcao@MyLinux:~/Linux/Lesson26$ cat /proc/sys/kernel/core_pattern 
core

之后在gdb中查看core文件:

boyangcao@MyLinux:~/Linux/Lesson26$ ./a.out 
段错误 (核心已转储)
boyangcao@MyLinux:~/Linux/Lesson26$ gdb a.out
(gdb) core-file core
[New LWP 8377]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault. # SIGSEGV: 进程进行了无效内存访问
#0  0x000055fc55bd6602 in main () at core.c:8
8           strcpy(buf, "Hello");


信号相关函数



发送信号函数:
int kill(pid_t pid, int sig);
int raise (int sig);
void abort (void);

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
        功能:给任何的进程或者进程组pid,发送某个信号sig
        参数:
            - pid: 需要发送给的进程的id
                > 0: 将信号发送给指定的进程
                = 0: 将信号发送给当前的进程组
                = -1: 将信号发送给每一个有权限接收这个信号的进程
                < -1: pid = 某个进程组的id取反 (-12345 即为id=12345的进程组) 
                    		给这个进程组所有进程发送信号
            - sig: 需要发送的信号的编号或者是宏值,0表示不发送任何信号
        eg.
            kill(getppid(), 9);
            kill(getpid(), 9);

int raise (int sig);
        功能:给当前进程发送信号
        参数:
            -sig: 需要发送的信号的编号或者是宏值,0表示不发送任何信号
        返回值:
            - 成功:0
            - 失败:nonzero
        eg.
            kill(getpid(), sig) 效果等同于 raise(sig)

void abort (void);
        功能:发送SIGABRT信号给当前进程,杀死当前进程
        eg.
            kill(getpid(), SIGABRT);



定时器函数:


alarm():

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
        功能: 设置定时器(闹钟) 函数调用,开始倒计时,
            当倒计时为0的时候,函数会给当前进程发送一个信号:SIGALRM
            -SIGALRM: 默认终止当前进程,每一个进程都有且只有唯一的一个定时器。
        参数:
            seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)
                取消一个定时器: 通过alarm(0);
        返回值:
            -之前没有定时器:返回0
            -之前有定时器:返回之前的定时器剩余时间
        eg1.
            alarm(10); // 返回0
            过了1s...
            alarm(5); // 返回上一个定时器剩余的时间 即为9
            alarm(100) -> 该函数是不阻塞的,可以继续执行其他代码。
        eg2.
            int main()
			{
    			int seconds = alarm(1);
    			for(int i = 0; ; i++) printf("%d\n", i);
    			return 0;
            }    
    程序实际运行的时间 = 内核时间 + 用户时间 + 消耗的时间(IO等)
    操作内存:
        内核时间:程序执行系统调用的时间
            (alarm()系统函数等 包括切换到内核运行到切换回用户区继续执行同样需要消耗时间)
        用户时间:程序正常代码向下运行,该进程占用CPU的使用时间
    操作硬件:
        IO等消耗的时间
    进行文件IO操作的时候比较浪费时间

    注意:定时器与进程的状态无关(采用自然定时法)。无论进程采用什么状态,alarm()都会计时。
         alarm的定时时间包含的是:用户+系统内核的运行时间 
        	alarm和setitimer(ITIMER_PROF) 共享同一个定时器


setitmer()

:

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
                    struct itimerval *old_value);
        功能:设置定时器(闹钟),可以替代alarm函数。精度是微秒,可以实现周期性的定时
        参数:
            - which: 定时器以 什么时间 计时
                ITIMER_REAL:    真实时间,每次时间到达发送 SIGALRM 信号 (常用!)
                ITIMER_VIRTUAL: 用户时间,每次时间到达发送 SIGVTALRM 信号
                ITIMER_PROF:    以该进程在用户态 + 内核态下所消耗的时间来计算,每次时间到达发送 SIGPROF 信号
            - new_value: 设置定时器属性
                struct itimerval {  // 定时器的结构体
                    struct timeval it_interval; // 每个阶段的时间,间隔时间
                    struct timeval it_value;    // 延迟多长时间执行定时器
                };
                eg. 过十秒后,每隔2秒定时一次。 it_interval = 2; it_value = 10;

                struct timeval {    // 时间的结构体
                    time_t      tv_sec;         // seconds 
                    suseconds_t tv_usec;        // microseconds 
                };
            - old_value: 记录上一次的定时的时间参数。一般不使用,指定 NULL
        返回值:
            成功:返回 0
            失败:返回-1,并设置errno

示例:

#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
// 过3秒以后每隔2秒钟定时1次
int main()
{
    struct itimerval new_value;
    // 设置间隔时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;
    // 设置延迟时间启动计时器(延迟时间到后发送第一个信号)
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;
    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了...\n");
    if(ret == -1)
    {
        perror("setitimer");
        exit(-1);
    }
    getchar();
    return 0;
}


信号捕捉函数:
#include <signal.h>
typedef void (*__sighandler_t)(int);
        函数指针:
            函数指针的名称:sighandler_t (函数的名称对应函数的地址)
            返回值:void
            参数:int 表示捕捉到的信号的值
__sighandler_t signal(int signum, __sighandler_t handler);
        功能:设置某个信号的捕捉行为
        参数:
            - signum: 要捕捉的信号 (最好使用宏值)
            - handler: 捕捉到信号要如何处理
                - SIG_IGN: 忽略信号
                - SIG_DFL: 使用信号默认行为
                - 回调函数:这个函数是内核调用 程序员只负责写这个函数,捕捉到信号后如何去处理信号
                    回调函数:
                        - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义。
                        - 不是程序员调用,而是当信号产生,由内核调用。
                        - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置即可。
        返回值:
            - 成功:返回上一次注册的信号处理函数的地址handler,第一次调用返回NULL
            - 失败:返回SIG_ERR,设置errno
        eg.
            void myalarm(int num)
            {
                printf("捕捉到了信号的编号是: %d\n", num);
                printf("xxxxxxx\n");
            }
            __sighandler_t res = signal(SIGALRM, myalarm);

    注意:SIGKILL SIGSTOP 不能被捕捉/忽略
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
        功能:检查或改变信号的处理,信号捕捉。
        参数:
            - signum: 需要捕捉的信号的编号或者宏值(信号的名称)
            - act: 捕捉到信号之后的处理动作
            - oldcat: 上一次对信号捕捉相关的设置。一般不使用,使用NULL。
        返回值:
            - 成功:返回0
            - 失败:返回-1
    
        struct sigaction {
                // 函数指针,指向的函数就是信号捕捉到后的处理函数
            void     (*sa_handler)(int);
                // (不常用) 
            void     (*sa_sigaction)(int, siginfo_t *, void *);
                // 临时阻塞信号集,信号捕捉函数执行过程中,临时阻塞某些信号
            sigset_t   sa_mask;
                // 使用哪一个信号处理函数对捕捉到的信号进行处理
                    // 这个值可以是 0,表示使用sa_handler
                    // 也可以是SA_SIGINFO,表示使用sa_sigaction
            int        sa_flags;
                // 已废弃,指定 NULL
            void     (*sa_restorer)(void);
        };

		eg.
            void myalarm(int num)
            {
                printf("捕捉到了信号的编号是: %d\n", num);
                printf("xxxxxxx\n");
            }
            int main()
            {
                struct sigaction act;
                act.sa_flags = 0;
                act.sa_handler = myalarm;
                // 清空临时阻塞信号集
                sigemptyset(&act.sa_mask);
                // 注册信号捕捉
                int res = sigaction(SIGALRM, &act, NULL);
                if(res == -1)
                {
                    perror("sigaction");
                    exit(-1);
                }
            }


内核实现信号捕捉的过程:

在这里插入图片描述


注意:

  1. 在执行信号捕捉处理函数的过程中,会使用临时阻塞信号集,当信号处理完成后,会恢复到内核(PCB内部)的阻塞信号集。
  2. 在执行某个回调函数时,比如捕捉SIGALRM信号,那么在对这个信号的处理过程中,会默认屏蔽该信号(如再发送来SIGALRM信号不会立即进行处理,需要将当前对这个信号的处理操作完成后再进行后来发送的该信号的处理)
  3. 阻塞的常规信号(0-31)是不支持排队的。因为未决信号集和阻塞信号集都是标志位,只能记录0 和1两个状态,如果短时间内发送大量相同信号,最多只能记录1次,其余都被丢弃掉了。


信号集
  • 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为sigset_t。
  • 在PCB中有两个非常重要的信号集。一个称之为“

    阻塞信号集

    ”,另一个称之为“

    未决信号集

    ”。这两个信号集都是内核使用

    位图机制

    (二进制位)来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改

    • (阻塞信号集可以被修改;未决信号集不能被修改,只能去获取查看)。
  • 信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
  • 信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
  • 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

在这里插入图片描述

  • 用户通过键盘 Ctrl + C,产生2号信号SIGINT (信号被创建)

  • 信号产生但是没有被处理 (未决)

    • 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
    • SIGINT 信号状态被存储在第二个标志位上

      • 这个标志位的值为0, 说明信号不是未决状态
      • 这个标志位的值为1, 说明信号处于未决状态
  • 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较

    • 阻塞信号集默认不阻塞任何的信号
    • 如果想要阻塞某些信号需要用户调用系统的API
  • 在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了

    • 如果没有阻塞,这个信号就被处理

    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理


以下信号集相关的函数都是对自定义的信号集进行操作

    int sigemptyset(sigset_t *set)
        功能:清空信号集中的数据,将信号集中的所有标志位 置为0
        参数:
            - set: 传出参数,需要操作的信号集
                sigset_t: 64位整数  
        返回值:
            - 成功:返回0
            - 失败:返回-1,设置errno
        eg.
            sigset_t set;
        	sigemptyset(&set);
    
    int sigfillset(sigset_t*set);
        功能:将信号集中的所有标志位 置为 1
        参数:
            - set: 传出参数,需要操作的信号集
        返回值:
            - 成功:返回0
            - 失败:返回-1,设置errno
        eg. sigfillset(&set);

    int sigaddset(sigset_t *set, int signum);
        功能:设置信号集中的某一个信号对应的标志位,将其置为1,表示阻塞该信号。
        参数:
            - set: 传出参数,需要操作的信号集
            - signum: 需要设置阻塞的信号
        返回值:
            - 成功:返回0
            - 失败:返回-1,设置errno
        eg. sigaddset(&set, SIGINT);

    int sigdelset(sigset_t *set,int signum);
        功能:设置信号集中的某一个信号对应的标志位,将其置为0,表示阻塞该信号。
        参数:
            - set: 传出参数,需要操作的信号集
            - signum: 需要设置不阻塞的信号
        返回值:
            - 成功:返回0
            - 失败:返回-1,设置errno
        eg. sigdelset(&set, SIGINT);

    int sigismember(const sigset_t *set, int signum);
        功能:判断某个信号是否阻塞
        参数:
            - set: 需要查看的信号集
            - signum: 需要判断的信号
        返回值:
            - 1:signum被阻塞
            - 0:signum不阻塞
            - -1:调用失败
        eg.
        	ret = sigismember(&set, SIGQUIT);
    		if(ret == 0) printf("SIGQUIT 不阻塞\n");
            else if(ret == 1) printf("SIGQUIT 阻塞\n");

如果需要修改内核中的阻塞信号集或者查看未决信号集,需要使用以下函数:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
        功能:将自定义信号集中的数据设置到内核中(设置阻塞;解除阻塞;替换)
        参数:
            - how: 如何对内核阻塞信号集进行处理
                SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变 (取按位或)
                    假设内核中默认的阻塞信号集是mask,  最后内核中的阻塞信号集 mask |= set
                SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞。
                    假设内核中默认的阻塞信号集是mask,  最后内核中的阻塞信号集 mask &= ~set
                SIG_SETMASK: 覆盖内核中原来的值
            - set: 已经初始化好的用户自定义的信号集
            - oldset: 保存设置之前的内核中的阻塞信号集的状态,可以是NULL。
        返回值:
            - 成功:返回0
            - 失败:返回-1,设置errno: (EFAULT or EINVAL)
        eg. sigprocmask(SIG_UNBLOCK, &set, NULL);

int sigpending(sigset_t *set);
        功能:获取内核中的未决信号集
        参数:
            - set: 传出参数,保存的是内核中的未决信号集中的信息。
        eg. sigpending(&pendingset);

案例:编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕

// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号
int main()
{
    // 设置2号信号和3号信号为阻塞
    sigset_t set;
    sigemptyset(&set);
    // 将2号和3号信号添加到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);
    // 修改内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &set, NULL);
    int num = 0;
    while(1)
    {
        // 获取当前的未决信号集中的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);
        num++;
        // 遍历前31位
        for(int i = 1; i < 32; i++)
        {
            if(sigismember(&pendingset, i) == 1) printf("1");
            else if(sigismember(&pendingset, i) == 0) printf("0");
            else{
                perror("sigismember");
                exit(-1);
            }
        }
        printf("\n");
        sleep(1);
        if(num == 10)
        {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
    }
    return 0;
}

注意:如果导入了#include <signal.h>头文件,仍然显示“

未定义标识符 “sigset_t”****C/C++(20)

那么如下设置:

#define _BSD_SOURCE (不建议)
#define _DEFAULT_SOURCE

In file included from /usr/include/signal.h:25:0,
                 from sigset.c:51:
/usr/include/features.h:184:3: warning: #warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE" [-Wcpp]
 # warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE"
   ^~~~~~~

注意:

# 加 & 号,在后台运行,在控制台可以继续进行输入指令
./main & 
# 让在后台运行的进程切换回前台: fg 
# 此时在控制台不可以输入指令
fg


SIGCHLD信号
  • SIGCHLD信号产生的条件

    • 子进程终止时
    • 子讲程接收到 SIGSTOP 信号停止时
    • 子进程处在停止态,接受到 SIGCONT 后唤醒时
  • 以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号


    # 使用SIGCHLD信号解决僵尸进程的问题

#define  _DEFAULT_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
void myFun(int num)
{
    printf("捕捉到的信号: %d\n", num);
    // 回收子进程PCB的资源
    // wait(NULL) // 这样同时有大量子进程结束时,只能回收一个进程资源,其余的都抛弃了
    // while(1) wait(NULL); // 这样的话虽然可以解决上面问题,但是这样会一直停留在回调函数,无法返回主控制流程
    while(1)
    {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret > 0)  printf("child die, pid = %d\n", ret);
        else if(ret == 0) break;  // 说明还有子进程活着
        else if(ret == -1) break; // 说明没有子进程了
    }
}
int main()
{
    // 提前设置好阻塞信号集,阻塞SIGCHLD
    // 因为有可能子进程很快结束,父进程还没有注册完信号捕捉
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);
    // 创建一些子进程
    __pid_t pid;
    for(int i = 0; i < 20; i++)
    {
        pid = fork();
        if(pid == 0) break;
    }
    if(pid > 0)
    {
        // 父进程
        // 捕捉子进程结束时发送的SIGCHLD信号
        struct sigaction act;
        act.sa_handler = myFun;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);
        // 注册完信号捕捉以后,解除阻塞。
        sigprocmask(SIG_UNBLOCK, &set, NULL);
        while(1) {
            printf("parent process pid: %d\n", getpid());
            sleep(2);
        }
    }
    else if(pid == 0) printf("child process pid: %d\n", getpid()); // 子进程
    return 0;


共享内存
  • 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分.因此这种IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
  • 与管道等 要求 发送进程将数据从用户空间的缓冲区复制进内核内存 和 接收进程 将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC技术的速度更快。


共享内存使用步骤:

  • 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
  • 使用 shmat() 来附上共享内存段.即使该段成为调用进程的虚拟内存的一部分。
  • 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat ()调用返回的 addr值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  • 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
  • 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。


共享内存相关的函数:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
    - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识(ID)。
        新创建的内存段中的数据都会被初始化为0
    - 参数:
        - key : key_t类型是一个整型,通过这个找到或者创建一个共享内存。
                一般使用16进制表示,非0- size: 共享内存的大小 (以实际分页的大小来创建分配(PAGE_SIZE 的整数倍))
        - shmflg: 属性
            - 访问权限 
            - 附加属性:创建 或者 判断共享内存是不是存在
                - 创建:IPC_CREAT
                - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
                    eg. IPC_CREAT | IPC_EXCL | 0664
        - 返回值:
            失败:-1 并设置错误号
            成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
        eg. int shmid = shmget(100, 4096, IPC_CREAT | 0664);
			int shmid = shmget(100, 0, IPC_CREAT); // size = 0 表示获取共享内存

void *shmat(int shmid, const void *shmaddr, int shmflg);
    - 功能:和当前的进程进行关联
    - 参数:
        - shmid : 共享内存的标识(ID),由shmget返回值获取
        - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
        - shmflg : 对共享内存的操作
            - 读 : SHM_RDONLY, 且必须要有读权限
            - 读写: 0
    - 返回值:
        成功:返回共享内存的首(起始)地址。  
        失败:返回 (void *) -1
    eg.void* ptr = shmat(shmid, NULL, 0);
 
int shmdt(const void *shmaddr);
    - 功能:解除当前进程和共享内存的关联
    - 参数:
        shmaddr:共享内存的首地址
    - 返回值:成功 0, 失败 -1
    eg. shmdt(ptr);

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    - 功能:对共享内存进行操作。(主要是删除共享内存,共享内存要删除才会消失)。
            创建共享内存的进程被销毁了对共享内存是没有任何影响的。
    - 参数:
        - shmid: 共享内存的ID
        - cmd : 要做的操作
            - IPC_STAT : 获取共享内存的当前的状态
            - IPC_SET : 设置共享内存的状态
            - IPC_RMID: 标记共享内存被销毁
                        (实际上只有所有进程都对该共享内存解除关联才能执行销毁操作,现在只是标记)
        - buf:需要设置或者获取的共享内存的属性信息
            - IPC_STAT : buf用来存储数据(传出参数)
            - IPC_SET : buf中需要初始化数据,设置到内核中
            - IPC_RMID : 没有用,NULL
    eg. shmctl(shmid, IPC_RMID, NULL); // 删除共享内存

       struct shmid_ds {
       struct ipc_perm shm_perm;    /* Ownership and permissions */
       size_t          shm_segsz;   /* Size of segment (bytes) */
       time_t          shm_atime;   /* Last attach time */
       time_t          shm_dtime;   /* Last detach time */
       time_t          shm_ctime;   /* Last change time */
       pid_t           shm_cpid;    /* PID of creator */
       pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
       shmatt_t        shm_nattch;  /* No. of current attaches */
       ...
   	   };

key_t ftok(const char *pathname, int proj_id);
    - 功能:根据指定的路径名,和int值,生成一个共享内存的key
    - 参数:
        - pathname:指定一个存在的路径
            /home/boyangcao/Linux/a.txt 
            / 
        - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
                   范围 : 0-255  一般指定一个字符 'a'


共享内存操作命令

ipcs用法:
	ipcs -a # 打印当前系统中所有的进程间通信方式的信息
    ipcs -m # 打印出使用共享内存进行进程间通信的信息
    ipcs -q # 打印出使用消息队列进行进程间通信的信息
    ipcs -s # 打印出使用信号进行进程间通信的信息
ipcrm用法:
ipcrm -M shmkey 	# 移除用shmkey创建的共享内存段
ipcrm -m shmid 		# 移除用shmid标识的共享内存段
ipcrm -Q msgkey		# 移除用msqkey创建的消息队列
ipcrm -q msqid		# 移除用msqid标识的消息队列
ipcrm -s semkey		# 移除用semkey创建的信号
ipcrm -s semid		# 移除用semid标识的信号
  • 问题1:操作系统如何知道一块共享内存被多少个进程关联?

    - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
        
        shm_nattch 记录了关联的进程个数
    
  • 问题2:可不可以对共享内存进行多次删除 shmctl

    • 可以的

    • 因为shmctl 标记删除共享内存,不是直接删除

    • 什么时候真正删除呢?

      当和共享内存关联的进程数为0的时候,就真正被删除

    • 当共享内存的key为0(0x00000000)的时候,表示共享内存被标记删除了

      如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能再次进行关联。

  • 共享内存和内存映射的区别

    1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)

    2.共享内存效果更高

    3.内存

    所有的进程操作的是同一块共享内存。

    内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。

    4.数据安全

    – 进程突然退出

    共享内存还存在

    内存映射区消失

    – 运行进程的电脑死机,宕机了

    数据存储在共享内存中,就没有了

    内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。

    5.生命周期

    – 内存映射区:进程退出,内存映射区销毁

    – 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机

    如果一个进程退出,会自动和共享内存进行取消关联。



版权声明:本文为beyon_sir原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。