Linux高并发服务器开发——Linux多进程开发

发布于:2024-04-29 ⋅ 阅读:(159) ⋅ 点赞:(0)

文章目录


在这里插入图片描述

进程概述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

单道、多道程序设计

在这里插入图片描述

时间片

在这里插入图片描述

并行和并发

在这里插入图片描述
在这里插入图片描述

进程控制块(PCB)

在这里插入图片描述
在这里插入图片描述

ulimit -a 显示当前所有的资源上限

进程可以使用的资源上限(Resource Limit)(指令:ulimit -a)

在这里插入图片描述

进程状态转换

进程的状态

在这里插入图片描述
在这里插入图片描述

进程相关命令

在这里插入图片描述

man ps

在这里插入图片描述

ps aux

ps aux

在这里插入图片描述
第一个会话终端
在这里插入图片描述
第二个会话终端
在这里插入图片描述
第三个会话终端
在这里插入图片描述

ps ajx

在这里插入图片描述

STAT参数意义

在这里插入图片描述

实时显示进程动态top

在这里插入图片描述

按u输入指定用户,比方这里我输入ubuntu

在这里插入图片描述

杀死进程

在这里插入图片描述

在第一个会话终端可以看到第二个会话终端的进程信息

ps aux

在这里插入图片描述

kill进程,发现第二个会话终端断联了

在这里插入图片描述

把当前进程强制杀死

kill -9 pid

或者

kill -SIGKILL pid

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

为什么kill后面要加-9

kill命令默认的信号是15,即kill -15,此时系统发送一个SIGTERM的信号给对应程序,当程序接收到该信号后,具体如何处理是自己可以决定的(可结束,可忽略),结束前一般会进行一些"准备工作"。由于SIGTERM(15)的信号可以被阻塞和忽略,这也导致有时候使用kill命令没办法"杀死"应用;

9 号信号能够强制杀死进程,系统会发出SIGKILL信号,要求接收到该信号的程序立即结束运行,不能被阻塞或忽略,这通常会带来一些副作用,因为应用程序没有时间进行"准备工作",可能造成数据丢失或者终端无法恢复到正常状态等;

man 3 sleep

在这里插入图片描述
test.c

#include <stdio.h>
  2 #include <unistd.h>
  3 
  4 int main() {
  5         while(1) {
  6                 printf("hw\n");
  7                 sleep(1);
  8         }
  9         return 0;
 10 }

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

& 让程序在后台运行

在这里插入图片描述

进程号和相关函数

在这里插入图片描述
在这里插入图片描述

进程创建

在这里插入图片描述
在这里插入图片描述

fork函数

pid_t fork(void);
函数的作用: 用于创建子进程
返回值:
fork()的返回值会返回两次. 一次是在父进程中, 一次是在子进程中.
在父进程中返回创建的子进程的ID,
在子进程中返回0
如何区分父进程和子进程: 通过fork的返回值.
在父进程中返回-1, 表示创建子进程失败, 并且设置errno

man 2 fork

在这里插入图片描述

pid_t类型的本质是int

在这里插入图片描述
在这里插入图片描述

fork.c

/*
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
        函数的作用: 用于创建子进程
        返回值: 
            fork()的返回值会返回两次. 一次是在父进程中, 一次是在子进程中.
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程: 通过fork的返回值.
            在父进程中返回-1, 表示创建子进程失败, 并且设置errno
*/

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

int main() {
    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {
        printf("pid : %d\n", pid);
        // 如果pid大于0, 返回的是创建的子进程的进程号, 当前是父进程
        printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
    } else if(pid == 0) {
        // 当前是子进程
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for(int i = 0; i < 5; i++) {
        printf("i : %d, pid : %d\n", i, getpid());
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述
验证了父进程的父进程就是当前终端
在这里插入图片描述

再编译运行一次

在这里插入图片描述

修改一下代码

/*
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
        函数的作用: 用于创建子进程
        返回值: 
            fork()的返回值会返回两次. 一次是在父进程中, 一次是在子进程中.
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程: 通过fork的返回值.
            在父进程中返回-1, 表示创建子进程失败, 并且设置errno
*/

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

int main() {
    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {
        printf("pid : %d\n", pid);
        // 如果pid大于0, 返回的是创建的子进程的进程号, 当前是父进程
        printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
    } else if(pid == 0) {
        // 当前是子进程
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for(int i = 0; i < 1000; i++) {
        printf("i : %d, pid : %d\n", i, getpid());
        // sleep(1);
    }

    return 0;
}

在这里插入图片描述

父子进程虚拟地址空间情况

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
fork.c

/*
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
        函数的作用: 用于创建子进程
        返回值: 
            fork()的返回值会返回两次. 一次是在父进程中, 一次是在子进程中.
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程: 通过fork的返回值.
            在父进程中返回-1, 表示创建子进程失败, 并且设置errno
*/

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

int main() {
    int num = 10;

    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {
        printf("pid : %d\n", pid);
        // 如果pid大于0, 返回的是创建的子进程的进程号, 当前是父进程
        printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
        printf("parent num: %d\n", num);
        num += 10;
        printf("parent num += 10 : %d\n", num);
    } else if(pid == 0) {
        // 当前是子进程
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());
        printf("child num: %d\n", num);
        num += 100;
        printf("child num += 100 : %d\n", num);
    }

    for(int i = 0; i < 3; i++) {
        printf("i : %d, pid : %d\n", i, getpid());
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

读时共享,写时拷贝

实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。

在这里插入图片描述
在这里插入图片描述

/*
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
        函数的作用: 用于创建子进程
        返回值: 
            fork()的返回值会返回两次. 一次是在父进程中, 一次是在子进程中.
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程: 通过fork的返回值.
            在父进程中返回-1, 表示创建子进程失败, 并且设置errno
*/

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

int main() {
    int num = 10;

    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {
        printf("pid : %d\n", pid);
        // 如果pid大于0, 返回的是创建的子进程的进程号, 当前是父进程
        printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
        printf("parent num: %d\n", num);
        num += 10;
        printf("parent num += 10 : %d\n", num);
        printf("Address of num in parent process: %p\n", &num);
    } else if(pid == 0) {
        // 当前是子进程
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());
        printf("child num: %d\n", num);
        num += 100;
        printf("child num += 100 : %d\n", num);
        printf("Address of num in child process: %p\n", &num);
    }

    for(int i = 0; i < 3; i++) {
        printf("i : %d, pid : %d\n", i, getpid());
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

最终打印结果发现两地址进程相同,这是因为我们所理解的是虚拟内存地址,虚拟内存地址每个进程都是共享的,而MMU(内存管理单元)所映射的物理内存地址是不一样的,通过写操作会拷贝数据到新的物理内存地址。

从操作系统来理解,每个进程有自己的页表,父进程 fork 出新的子进程时,子进程拷贝一份父进程的页表,且父子进程将页表状态修改为写保护。当父进程或子进程发生写操作时将会发生缺页异常,缺页异常处理函数将会为子进程分配新的物理地址。由于不同的进程的页表不同,因此访问同样的逻辑地址对应的物理地址才不同。

父子进程之间的关系

区别

在这里插入图片描述

共同

在这里插入图片描述

父子进程对变量是不是共享的?

刚开始是共享的,一旦修改数据共享不了。读时共享(子进程被创建,两个进程没有做任何写操作),写时拷贝

GDB多进程调试

工作一般不用,面试较多

在这里插入图片描述
hello.c

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

int main() {

    printf("begin\n");

    if(fork() > 0) {

        printf("I am parent process: pid = %d, ppid = %d\n", getpid(), getppid());

        int i;
        for(i = 0; i < 10; i++) {
            printf("i = %d\n", i);
            sleep(1);
        }
    } else {
        printf("I am child process: pid = %d, ppid = %d\n", getpid(), getppid());
        
        int j;
        for(j = 0; j < 10; j++) {
            printf("j = %d\n", j);
            sleep(1);
        }
    }

    return 0;
}

在这里插入图片描述

默认跟踪父进程

在这里插入图片描述

set follow-fork-mode [parent(默认)| child]

在这里插入图片描述

set detach-on-fork [on | off]

在这里插入图片描述

查看调试的进程: info inferiors

在这里插入图片描述
在这里插入图片描述

detach inferiors id

在这里插入图片描述

升级gdb

看我这篇博客很详细

exec函数族

读时共享、写时拷贝针对的是物理内存,只是在读时会有两个独立的虚拟内存空间,指向同一个物理内存。

执行 exec ,会复制一份物理内存。

在这里插入图片描述
在这里插入图片描述

exec函数族作用图解

在这里插入图片描述
在这里插入图片描述

七个函数

在这里插入图片描述

execl函数

man 3 execl

在这里插入图片描述
hello.c

#include <stdio.h>

int main() {
    printf("hw\n");
    return 0;
}

execl.c

/*
    #include <unistd.h>
    extern char **environ;
    int execl(const char *path, const char *arg, ...);
        参数: 
            - path: 需要指定的执行的文件的路径或者名称
                a.out  /home/ubuntu/a.out   推荐使用绝对路径
                ./a.out hello

            - arg: 是执行可执行文件所需的参数列表
                第一个参数一般没有什么作用, 为了方便, 一般写的是执行的程序名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)
        返回值:
            只有调用失败才会有返回值, 返回-1, 并且设置errno
            如果调用成功, 没有返回值
*/
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
extern char **environ;

int main() {
    // 创建一个子进程, 在子进程中执行exec函数族中的函数
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
        sleep(1);
    } else if(pid == 0) {
        // 子进程
        execl("hello", "hello", NULL);
        printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
    }

    for(int i = 0; i < 3; i++) {
        printf("i = %d, pid = %d\n", i, getpid());
    }

    return 0;
}

在这里插入图片描述

在这里插入图片描述

execl.c

/*
    #include <unistd.h>
    extern char **environ;
    int execl(const char *path, const char *arg, ...);
        参数: 
            - path: 需要指定的执行的文件的路径或者名称
                a.out  /home/ubuntu/a.out   推荐使用绝对路径
                ./a.out hello

            - arg: 是执行可执行文件所需的参数列表
                第一个参数一般没有什么作用, 为了方便, 一般写的是执行的程序名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)
        返回值:
            只有调用失败才会有返回值, 返回-1, 并且设置errno
            如果调用成功, 没有返回值
*/
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
extern char **environ;

int main() {
    // 创建一个子进程, 在子进程中执行exec函数族中的函数
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
        sleep(1);
    } else if(pid == 0) {
        // 子进程
        // execl("hello", "hello", NULL);
        execl("/bin/ps", "ps", "aux", NULL);

        printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
    }

    for(int i = 0; i < 3; i++) {
        printf("i = %d, pid = %d\n", i, getpid());
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述

execlp函数

int execlp(const char *file, const char *arg, …);
- 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功
参数:
- file: 需要执行的可执行文件的文件名
a.out
ps
- arg: 是执行可执行文件所需的参数列表
第一个参数一般没有什么作用, 为了方便, 一般写的是执行的程序名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
返回值:
只有调用失败才会有返回值, 返回-1, 并且设置errno
如果调用成功, 没有返回值

man 3 execlp

在这里插入图片描述

execlp.c

/*
    #include <unistd.h>
    extern char **environ;       
    int execlp(const char *file, const char *arg, ...);
        - 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功
        参数: 
            - file: 需要执行的可执行文件的文件名
                a.out
                ps

            - arg: 是执行可执行文件所需的参数列表
                第一个参数一般没有什么作用, 为了方便, 一般写的是执行的程序名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)
        返回值:
            只有调用失败才会有返回值, 返回-1, 并且设置errno
            如果调用成功, 没有返回值

*/

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
extern char **environ;

int main() {
    // 创建一个子进程, 在子进程中执行exec函数族中的函数
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
        sleep(1);
    } else if(pid == 0) {
        // 子进程
        // execl("hello", "hello", NULL);
        // execl("/bin/ps", "ps", "aux", NULL);
        execlp("ps", "ps", "aux", NULL);

        printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
    }

    for(int i = 0; i < 3; i++) {
        printf("i = %d, pid = %d\n", i, getpid());
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

其他函数

int execv(const char *path, char *const argv[]);
        argv是需要的参数的一个字符串数组
        char * argv[] = {"ps", "aux", NULL};
        execv("/bin/ps", argv);

int execve(const char *filename, char *const argv[], char *const envp[]);
    char * envp[] = {"/home/ubuntu", "/home/bbb", "/home/aaa"};

which ps查找命令ps文件存放目录

在这里插入图片描述

进程退出、孤儿进程、僵尸进程

进程退出

在这里插入图片描述

exit函数

exit.c

/*
    #include <stdlib.h>
    void exit(int status);

    #include <unistd.h>
    void _exit(int status);

    status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。

*/

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

int main() {
    
    printf("fang\n");
    printf("shuang");

    exit(0);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

_exit函数

exit.c

/*
    #include <stdlib.h>
    void exit(int status);

    #include <unistd.h>
    void _exit(int status);

    status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。

*/

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

int main() {
    
    printf("fang\n");
    printf("shuang");

    // exit(0);
    _exit(0);

    return 0;
}

在这里插入图片描述
注意:exit() 在调用 _exit() 之前会进行刷新I/O缓冲,由于当 std::endl 或者 \n 被输出时,缓冲区会被刷新,所以数据会被立即显示在屏幕上,而直接调用 _exit() 不会进行刷新I/O缓冲,所以当 std::endl 或者 \n 未被输出时数据不会被显示在屏幕上。

孤儿进程

在这里插入图片描述
orphan.c

/*
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
        函数的作用: 用于创建子进程
        返回值: 
            fork()的返回值会返回两次. 一次是在父进程中, 一次是在子进程中.
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程: 通过fork的返回值.
            在父进程中返回-1, 表示创建子进程失败, 并且设置errno
*/

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

int main() {
    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {
        // printf("pid : %d\n", pid);
        // 如果pid大于0, 返回的是创建的子进程的进程号, 当前是父进程
        printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
    } else if(pid == 0) {
        // 当前是子进程
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());

    }

    for(int i = 0; i < 3; i++) {
        printf("i : %d, pid : %d\n", i, getpid());
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述
orphan.c

/*
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
        函数的作用: 用于创建子进程
        返回值: 
            fork()的返回值会返回两次. 一次是在父进程中, 一次是在子进程中.
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程: 通过fork的返回值.
            在父进程中返回-1, 表示创建子进程失败, 并且设置errno
*/

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

int main() {
    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {
        // printf("pid : %d\n", pid);
        // 如果pid大于0, 返回的是创建的子进程的进程号, 当前是父进程
        printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
    } else if(pid == 0) {
        // 当前是子进程
        sleep(1);
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for(int i = 0; i < 3; i++) {
        printf("i : %d, pid : %d\n", i, getpid());
    }

    return 0;
}

在这里插入图片描述

僵尸进程

子进程终止时,父进程尚未回收还在运行,子进程残留资源(pcb)存放于内核中,变成僵尸进程(Z+),不能被kill -9杀死,只能父进程调用wait或者手动ctrl+c终止

在这里插入图片描述
zombie.c

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

int main() {
    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {
        // printf("pid : %d\n", pid);
        // 如果pid大于0, 返回的是创建的子进程的进程号, 当前是父进程
        while(1) {
            sleep(1);
            printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
        }
    } else if(pid == 0) {
        // 当前是子进程
        // sleep(1);
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for(int i = 0; i < 3; i++) {
        printf("i : %d, pid : %d\n", i, getpid());
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述

ps aux

在这里插入图片描述
kill -9 pid也杀不死僵尸进程

kill -9 30426

在这里插入图片描述
ctrl + c终止父进程,僵尸进程由1进程回收
在这里插入图片描述
在这里插入图片描述

进程回收

在这里插入图片描述

wait.c

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束, 如果任意一个子进程结束了, 此函数会回收子进程的资源
        参数:int *wstatus
            进程退出时的状态信息, 传入的是一个int类型的地址, 传出参数。
        返回值:
            - 成功:返回被回收的子进程的id
            - 失败:-1 (所有的子进程都结束,调用函数失败)

    调用wait函数的进程会被挂起(阻塞), 直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
    如果没有子进程了, 函数立刻返回, 返回-1; 如果子进程都已经结束了, 也会立即返回, 返回-1

*/

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

int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }
    
    if(pid > 0) {
        // 父进程
        while(1) {
            sleep(1);
            printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
        }
    } else if(pid == 0) {
        // 子进程
        printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述

wait函数

在这里插入图片描述

pid_t wait(int *wstatus);
    功能:等待任意一个子进程结束, 如果任意一个子进程结束了, 此函数会回收子进程的资源
    参数:int *wstatus
        进程退出时的状态信息, 传入的是一个int类型的地址, 传出参数。
    返回值:
        - 成功:返回被回收的子进程的id
        - 失败:-1 (所有的子进程都结束,调用函数失败)
man 2 wait

在这里插入图片描述
wait.c

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束, 如果任意一个子进程结束了, 此函数会回收子进程的资源
        参数:int *wstatus
            进程退出时的状态信息, 传入的是一个int类型的地址, 传出参数。
        返回值:
            - 成功:返回被回收的子进程的id
            - 失败:-1 (所有的子进程都结束,调用函数失败)

    调用wait函数的进程会被挂起(阻塞), 直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
    如果没有子进程了, 函数立刻返回, 返回-1; 如果子进程都已经结束了, 也会立即返回, 返回-1

*/

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

int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }
    
    if(pid > 0) {
        // 父进程
        while(1) {
            sleep(2);
            printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
            int ret = wait(NULL);
            if(ret == -1) {
                break;
            }
            printf("child die, pid = %d\n", ret);
        }
    } else if(pid == 0) {
        // 子进程
        while(1) {
            sleep(1);
            printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
        }
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

退出信息相关宏函数

在这里插入图片描述

WIFEXITED(status)和WEXITSTATUS(status)

wait.c

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束, 如果任意一个子进程结束了, 此函数会回收子进程的资源
        参数:int *wstatus
            进程退出时的状态信息, 传入的是一个int类型的地址, 传出参数。
        返回值:
            - 成功:返回被回收的子进程的id
            - 失败:-1 (所有的子进程都结束,调用函数失败)

    调用wait函数的进程会被挂起(阻塞), 直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
    如果没有子进程了, 函数立刻返回, 返回-1; 如果子进程都已经结束了, 也会立即返回, 返回-1

*/

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

int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }
    
    if(pid > 0) {
        // 父进程
        while(1) {
            sleep(2);
            printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
            // int ret = wait(NULL);
            int st;
            int ret = wait(&st);

            if(ret == -1) {
                break;
            }

            if(WIFEXITED(st)) {
                // 是不是正常退出
                printf("Exit status code: %d\n", WEXITSTATUS(st));
            }
            if(WIFSIGNALED(st)) {
                // 是不是异常中止
                printf("Which signal killed: %d\n", WTERMSIG(st));
            }

            printf("\nchild die, pid = %d\n", ret);
        }
    } else if(pid == 0) {
        // 子进程
        // while(1) {
            sleep(1);
            printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
        // }
        exit(0);
    }

    return 0;   // 相当于exit(0)
}

在这里插入图片描述

wait.c

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束, 如果任意一个子进程结束了, 此函数会回收子进程的资源
        参数:int *wstatus
            进程退出时的状态信息, 传入的是一个int类型的地址, 传出参数。
        返回值:
            - 成功:返回被回收的子进程的id
            - 失败:-1 (所有的子进程都结束,调用函数失败)

    调用wait函数的进程会被挂起(阻塞), 直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
    如果没有子进程了, 函数立刻返回, 返回-1; 如果子进程都已经结束了, 也会立即返回, 返回-1

*/

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

int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }
    
    if(pid > 0) {
        // 父进程
        while(1) {
            sleep(2);
            printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
            // int ret = wait(NULL);
            int st;
            int ret = wait(&st);

            if(ret == -1) {
                break;
            }

            if(WIFEXITED(st)) {
                // 是不是正常退出
                printf("Exit status code: %d\n", WEXITSTATUS(st));
            }
            if(WIFSIGNALED(st)) {
                // 是不是异常中止
                printf("Which signal killed: %d\n", WTERMSIG(st));
            }

            printf("\nchild die, pid = %d\n", ret);
        }
    } else if(pid == 0) {
        // 子进程
        // while(1) {
            sleep(1);
            printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
        // }
        exit(1);
    }

    return 0;   // 相当于exit(0)
}

在这里插入图片描述

WIFSIGNALED(status)和WTERMSIG(status)

wait.c

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束, 如果任意一个子进程结束了, 此函数会回收子进程的资源
        参数:int *wstatus
            进程退出时的状态信息, 传入的是一个int类型的地址, 传出参数。
        返回值:
            - 成功:返回被回收的子进程的id
            - 失败:-1 (所有的子进程都结束,调用函数失败)

    调用wait函数的进程会被挂起(阻塞), 直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
    如果没有子进程了, 函数立刻返回, 返回-1; 如果子进程都已经结束了, 也会立即返回, 返回-1

*/

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

int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }
    
    if(pid > 0) {
        // 父进程
        while(1) {
            sleep(2);
            printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
            // int ret = wait(NULL);
            int st;
            int ret = wait(&st);

            if(ret == -1) {
                break;
            }

            if(WIFEXITED(st)) {
                // 是不是正常退出
                printf("Exit status code: %d\n", WEXITSTATUS(st));
            }
            if(WIFSIGNALED(st)) {
                // 是不是异常中止
                printf("Which signal killed: %d\n", WTERMSIG(st));
            }

            printf("\nchild die, pid = %d\n", ret);
        }
    } else if(pid == 0) {
        // 子进程
        while(1) {
            sleep(1);
            printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
        }
        exit(0);
    }

    return 0;   // 相当于exit(0)
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

exit(0)和exit(1)的区别

在这里插入图片描述
在这里插入图片描述

waitpid函数

pid_t waitpid(pid_t pid, int *wstatus, int options);
    功能:回收指定进程号的子进程,可以设置是否阻塞。
    参数:
        - pid:
            pid > 0 : 某个子进程的pid
            pid = 0 : 回收当前进程组的所有子进程    
            pid = -1 : 回收所有的子进程,相当于 wait()  (最常用)
            pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
        - options:设置阻塞或者非阻塞
            0 : 阻塞
            WNOHANG : 非阻塞(return immediately if no child has exited.)
        - 返回值:
            > 0 : 返回子进程的id
            = 0 : options=WNOHANG, 表示还有子进程活着
            = -1 :错误,或者没有子进程了

waitpid一次回收一个进程资源

man 2 waitpid

在这里插入图片描述

ps ajx

选择的是组进程的一个例子,15960就是一个组进程,bash是组长,所以pid和pgid一样

在这里插入图片描述

阻塞模式下

waitpid.c

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *wstatus, int options);
        功能:回收指定进程号的子进程,可以设置是否阻塞。
        参数:
            - pid:
                pid > 0 : 某个子进程的pid
                pid = 0 : 回收当前进程组的所有子进程    
                pid = -1 : 回收所有的子进程,相当于 wait()  (最常用)
                pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
            - options:设置阻塞或者非阻塞
                0 : 阻塞
                WNOHANG : 非阻塞(return immediately if no child has exited.)
            - 返回值:
                > 0 : 返回子进程的id
                = 0 : options=WNOHANG, 表示还有子进程活着
                = -1 :错误,或者没有子进程了

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

int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }
    
    if(pid > 0) {
        // 父进程
        while(1) {
            sleep(2);
            printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
            // int ret = wait(NULL);
            int st;
            int ret = waitpid(-1, &st, 0);

            if(ret == -1) {
                break;
            }

            if(WIFEXITED(st)) {
                // 是不是正常退出
                printf("Exit status code: %d\n", WEXITSTATUS(st));
            }
            if(WIFSIGNALED(st)) {
                // 是不是异常中止
                printf("Which signal killed: %d\n", WTERMSIG(st));
            }

            printf("\nchild die, pid = %d\n", ret);
        }
    } else if(pid == 0) {
        // 子进程
        while(1) {
            sleep(1);
            printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
        }
        exit(0);
    }

    return 0;   // 相当于exit(0)
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

非阻塞模式下

waitpid.c

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *wstatus, int options);
        功能:回收指定进程号的子进程,可以设置是否阻塞。
        参数:
            - pid:
                pid > 0 : 某个子进程的pid
                pid = 0 : 回收当前进程组的任意子进程    
                pid = -1 : 回收任意的子进程,相当于 wait()  (最常用)
                pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
            - options:设置阻塞或者非阻塞
                0 : 阻塞
                WNOHANG : 非阻塞(return immediately if no child has exited.)
            - 返回值:
                > 0 : 返回子进程的id
                = 0 : options=WNOHANG, 表示还有子进程活着
                = -1 :错误,或者没有子进程了

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

int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;

    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }
    
    if(pid > 0) {
        // 父进程
        while(1) {
            sleep(2);
            printf("I am parent process, pid : %d, ppid: %d\n", getpid(), getppid());
            // int ret = wait(NULL);
            int st;
            // int ret = waitpid(-1, &st, 0);
            int ret = waitpid(-1, &st, WNOHANG);

            if(ret == -1) {
                break;
            }

            if(ret == 0) {
                // 说明还有子进程存在
                continue;
            } else if(ret > 0) {
                if(WIFEXITED(st)) {
                    // 是不是正常退出
                    printf("Exit status code: %d\n", WEXITSTATUS(st));
                }
                if(WIFSIGNALED(st)) {
                    // 是不是异常中止
                    printf("Which signal killed: %d\n", WTERMSIG(st));
                }
                printf("\nchild die, pid = %d\n", ret);
            }
        }
    } else if(pid == 0) {
        // 子进程
        while(1) {
            sleep(1);
            printf("I am child process, pid : %d, ppid: %d\n", getpid(), getppid());
        }
        exit(0);
    }

    return 0;   // 相当于exit(0)
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

进程间通信简介

在这里插入图片描述

Linux进程间通信的方式

在这里插入图片描述

怎样理解阻塞非阻塞与同步异步的区别?

阻塞和非阻塞,描述的是一种状态,同步与异步描述的是行为方式

同步与异步

在这里插入图片描述

阻塞与非阻塞

在这里插入图片描述

匿名管道概述

在这里插入图片描述
在这里插入图片描述

管道的特点

在这里插入图片描述
在这里插入图片描述

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

文件描述符表在内核当中

在这里插入图片描述

管道的数据结构

在这里插入图片描述

匿名管道的使用

在这里插入图片描述

父子进程通过匿名管道通信

pipe函数

注意:匿名管道只能用于具有关系的进程之间的通信(父子进程、兄弟进程)。管道中如果没有信息时 read 操作是阻塞的,管道中如果信息已满 write 操作是阻塞,不能够同时进行读写,否则会一直阻塞。

man 2 pipe

在这里插入图片描述
pipe.c

/*
    #include <unistd.h>
    int pipe(int pipefd[2]);
        功能: 创建一个匿名管道, 用来进程间通信
        参数: int pipefd[2] 这个数组是一个传出参数
            pipefd[0] 对应的是管道的读端
            pipefd[1] 对应的是管道的写端
        返回值:
            成功 0
            失败 -1
            On success, zero is returned. On error, -1 is returned, and errno is set  appropriately.

    管道默认是阻塞的: 如果管道中没有数据, read阻塞, 如果管道满了, write阻塞

    注意: 匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)

*/

#include <unistd.h>
#include <stdio.h>
#include <sys/types.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) {
        // 父进程
        // 从管道的读取端读取数据
        char buf[1024] = {0};
        int len = read(pipefd[0], buf, sizeof(buf));
        printf("parent receive : %s, pid : %d\n", buf, getpid());
    } else if(pid == 0) {
        // 子进程
        char *str = "I love fangshuang!";
        write(pipefd[1], str, strlen(str));
    }

    return 0;
}

在这里插入图片描述

pipe.c

/*
    #include <unistd.h>
    int pipe(int pipefd[2]);
        功能: 创建一个匿名管道, 用来进程间通信
        参数: int pipefd[2] 这个数组是一个传出参数
            pipefd[0] 对应的是管道的读端
            pipefd[1] 对应的是管道的写端
        返回值:
            成功 0
            失败 -1
            On success, zero is returned. On error, -1 is returned, and errno is set  appropriately.

    管道默认是阻塞的: 如果管道中没有数据, read阻塞, 如果管道满了, write阻塞

    注意: 匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)

*/

#include <unistd.h>
#include <stdio.h>
#include <sys/types.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, ppid: %d\n", getpid(), getppid());
        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent receive : %s, pid : %d\n", buf, getpid());
            bzero(buf, 1024);
            // 向管道中写入数据
            char *str = "I love azhong!";
            write(pipefd[1], str, strlen(str));
            sleep(1);
        }
    } else if(pid == 0) {
        // 子进程
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());
        char buf[1024] = {0};
        while(1) {
            // 向管道中写入数据
            char *str = "I love fangshuang!";
            write(pipefd[1], str, strlen(str));
            sleep(1);

            int len = read(pipefd[0], buf, sizeof(buf));
            printf("child receive : %s, pid : %d\n", buf, getpid());
            bzero(buf, 1024);
        }
    }

    return 0;
}

在这里插入图片描述

查看管道缓冲大小命令ulimit

ulimit -a

在这里插入图片描述

查看管道缓冲大小函数fpathconf

man fpathconf

在这里插入图片描述
在这里插入图片描述
fpathconf.c

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

int main() {
    int pipefd[2];

    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 获取管道的大小
    long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
    printf("pipe size: %ld\n", size);

    return 0;
}

在这里插入图片描述

匿名管道通信案例

bzero函数

man bzero

在这里插入图片描述

匿名管道的使用

在这里插入图片描述
在这里插入图片描述
pipe.c

/*
    #include <unistd.h>
    int pipe(int pipefd[2]);
        功能: 创建一个匿名管道, 用来进程间通信
        参数: int pipefd[2] 这个数组是一个传出参数
            pipefd[0] 对应的是管道的读端
            pipefd[1] 对应的是管道的写端
        返回值:
            成功 0
            失败 -1
            On success, zero is returned. On error, -1 is returned, and errno is set  appropriately.

    管道默认是阻塞的: 如果管道中没有数据, read阻塞, 如果管道满了, write阻塞

    注意: 匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)

*/

#include <unistd.h>
#include <stdio.h>
#include <sys/types.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, ppid: %d\n", getpid(), getppid());

        // 关闭写端
        close(pipefd[1]);

        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent receive : %s, pid : %d\n", buf, getpid());
            bzero(buf, 1024);
            // 向管道中写入数据
            // char *str = "I love azhong!";
            // write(pipefd[1], str, strlen(str));
            // sleep(1);
        }
    } else if(pid == 0) {
        // 子进程
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());

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

        // char buf[1024] = {0};
        while(1) {
            // 向管道中写入数据
            char *str = "I love fangshuang!\n";
            write(pipefd[1], str, strlen(str));
            // sleep(1);

            // int len = read(pipefd[0], buf, sizeof(buf));
            // printf("child receive : %s, pid : %d\n", buf, getpid());
            // bzero(buf, 1024);
        }
    }

    return 0;
}

案例实现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

pci.c

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

#define BUF_SIZE 65544

int main()
{
	int pipeFd[2];
	if(pipe(pipeFd) == -1)
	{
		perror("pipe");
		exit(-1);
	}

	// 设置管道为非阻塞
	for(int i = 0; i < 2; ++i)
	{
		int flags = fcntl(pipeFd[i], F_GETFL);
		if(fcntl(pipeFd[i], F_SETFL, flags | O_NONBLOCK) == -1)
			perror("fcntl");
	}

	int pid = fork();
	if(pid > 0)
	{
		sleep(1);// 确保子进程先写。管道才有数据能读
		// 父进程读
		close(pipeFd[1]);
		char buf[BUF_SIZE + 1] = {0};
		printf("parent process reads: %ld bytes\n", read(pipeFd[0], buf, BUF_SIZE));
	}
	else if(pid == 0)
	{
		// 子进程
		close(pipeFd[0]);
        // in.txt有65544字节的数据
		int fd = open("in.txt", O_RDONLY);
		char buf[BUF_SIZE + 1] = {0};
		printf("read from file: %ld bytes\n", read(fd, buf, BUF_SIZE));
		printf("child process writes: %ld bytes\n", write(pipeFd[1], buf, BUF_SIZE));
	}
	else
	{
		perror("fork");
		exit(-1);
	}
	return 0;
}

parent-child-ipc.c

/*
    实现 ps aux | grep xxx 父子进程间通信
    
    子进程: ps aux, 子进程结束后, 将数据发送给父进程
    父进程: 获取到数据, 过滤
    pipe()
    execlp()
    子进程将标准输出 stdout_fileno 重定向到管道的写端.  dup2

*/

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/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))) > 0) {
            // 过滤数据输出
            printf("%s", buf);
            memset(buf, 0, 1024);
        }
        wait(NULL);

    } else if(pid == 0) {
        // 子进程
        // 关闭读端
        close(fd[0]);
        // 文件描述符的重定向 stdout_fileno -> fd[1]
        dup2(fd[1], STDOUT_FILENO);

        // 执行ps aux
        execlp("ps", "ps", "aux", NULL);
        perror("execlp");
        exit(0);
    } else{
        perror("fork");
        exit(0);
    }

    return 0;
}

在这里插入图片描述
在这里插入图片描述

实现过滤功能

/*
    实现 ps aux | grep xxx 父子进程间通信
    
    子进程: ps aux, 子进程结束后, 将数据发送给父进程
    父进程: 获取到数据, 过滤
    pipe()
    execlp()
    子进程将标准输出 stdout_fileno 重定向到管道的写端.  dup2

*/

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

int main(int argc, char * argv[]) {
    if(argc != 2) {
        printf("%s instructions\n", argv[0]);
        return -1;
    }
    // 创建一个管道
    int fd[2];
    int ret = pipe(fd);

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

    // 设置管道为非阻塞
	for(int i = 0; i < 2; ++i)
	{
		int flags = fcntl(fd[i], F_GETFL);
		if(fcntl(fd[i], F_SETFL, flags | O_NONBLOCK) == -1)
			perror("fcntl");
	}

    // 创建子进程
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        // 关闭写端
        close(fd[1]);
        wait(NULL);

        // 文件描述符的重定向 stdin_fileno -> fd[0]
        dup2(fd[0], STDIN_FILENO);

        int res = execlp("grep", "grep", argv[1], NULL);
        perror("execlp");
        exit(-1);
    } else if(pid == 0) {
        // 子进程
        // 关闭读端
        close(fd[0]);
        // 文件描述符的重定向 stdout_fileno -> fd[1]
        dup2(fd[1], STDOUT_FILENO);

        // 执行ps aux
        execlp("ps", "ps", "aux", NULL);
        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(相当于读到文件的末尾)
    - 写端没有完全关闭,read阻塞等待

  • 写管道:
    管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
    管道读端没有全部关闭:
    - 管道已满,write阻塞
    - 管道没有满,write将数据写入,并返回实际写入的字节数

默认阻塞状态下

noblock.c

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.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, ppid: %d\n", getpid(), getppid());

        // 关闭写端
        close(pipefd[1]);

        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent receive : %s, pid : %d\n", buf, getpid());
            bzero(buf, 1024);
        }
    } else if(pid == 0) {
        // 子进程
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());

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

        // char buf[1024] = {0};
        while(1) {
            // 向管道中写入数据
            char *str = "I love fangshuang!\n";
            write(pipefd[1], str, strlen(str));
            sleep(2);
        }
    }
    return 0;
}

在这里插入图片描述

非阻塞状态下

正常阻塞情况下,如果子进程休眠,这时写管道没关闭,父进程read应该阻塞,但是在非阻塞情况下不会阻塞,它发现写端没有关闭,认为会有进程写数据,所以从头开始读,但是没读取到,所以会读取失败返回-1。如果子进程结束,那么写端关闭,父进程的读端知道不会有进程写数据,所以不会从头开始接着读,一直停留在文件末尾读取数据,所以一直返回0.

noblock.c

#include <unistd.h>
#include <stdio.h>
#include <sys/types.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, ppid: %d\n", getpid(), getppid());

        // 关闭写端
        close(pipefd[1]);

        // 从管道的读取端读取数据
        char buf[1024] = {0};

        // 设置管道非阻塞
        int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
        flags |= O_NONBLOCK;                // 修改flag的值
        fcntl(pipefd[0], F_SETFL, flags);       // 设置新的flag

        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len : %d\n", len);
            printf("parent receive : %s, pid : %d\n", buf, getpid());
            bzero(buf, 1024);
            sleep(1);
        }
    } else if(pid == 0) {
        // 子进程
        printf("I am child process, pid : %d, ppid : %d\n", getpid(), getppid());

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

        // char buf[1024] = {0};
        while(1) {
            // 向管道中写入数据
            char *str = "I love fangshuang!\n";
            write(pipefd[1], str, strlen(str));
            sleep(3);
        }
    }
    return 0;
}

在这里插入图片描述

有名管道介绍及使用

在这里插入图片描述
在这里插入图片描述

有名管道的使用

在这里插入图片描述

mkfifo函数

在这里插入图片描述

man 3 mkfifo

在这里插入图片描述
mkfifo.c

/*
    创建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, 并设置错误号

*/

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

int main() {

    // 判断文件是否存在
    int res = access("fifo1", F_OK);
    if(res == -1) {
        printf("pipe not exists, create pipe...\n");
        int ret = mkfifo("fifo1", 0664);
        if(ret == -1) {
            perror("mkfifo");
            exit(0);
        }
    }
    return 0;
}

在这里插入图片描述
write.c

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

// 向管道中写数据
/*
    有名管道的注意事项:
        1.一个为只读而打开一个管道的进程会阻塞, 直到另外一个进程为只写打开管道
        2.一个为只写而打开一个管道的进程会阻塞, 直到另外一个进程为只读打开管道

    读管道:
        管道中有数据, read返回实际读到的字节数
        管道中无数据:
            管道写端被全部关闭, read返回0, (相当于读到文件末尾)
            写端没有全部被关闭, read阻塞等待
    
    写管道:
        管道读端被全部关闭, 进行异常终止(收到一个SIGPIPE信号)
        管道读端没有全部关闭:
            管道已经满了, write会阻塞
            管道没有满, write将数据写入, 并返回实际写入的字节数。
*/

int main() {
    // 1.判断文件是否存在
    int res = access("test", F_OK);
    if(res == -1) {
        printf("pipe not exists, create pipe...\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);
    }

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

    return 0;
}

read.c

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

// 从管道中读取数据
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("The write end is disconnected...\n");
            break;
        }
        printf("receive buf: %s\n", buf);
    }
    close(fd);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

fifo手册内容

man fifo
	A  FIFO  special  file  (a named pipe) is similar to a pipe, except that it is accessed as
   part of the filesystem.  It can be opened by multiple processes for  reading  or  writing.
   When  processes  are  exchanging  data via the FIFO, the kernel passes all data internally
   without writing it to the filesystem.  Thus, the FIFO special file has no contents on  the
   filesystem;  the filesystem entry merely serves as a reference point so that processes can
   access the pipe using a name in the filesystem.

   The kernel maintains exactly one pipe object for each FIFO special file that is opened  by
   at  least  one process.  The FIFO must be opened on both ends (reading and writing) before
   data can be passed.  Normally, opening the FIFO blocks until the other end is opened also.

   A process can open a FIFO in nonblocking mode.  In this case, opening for  read-only  suc‐
   ceeds  even  if  no  one has opened on the write side yet and opening for write-only fails
   with ENXIO (no such device or address) unless the other end has already been opened.

   Under Linux, opening a FIFO for read and write will succeed both in blocking and nonblock‐
   ing  mode.   POSIX  leaves  this  behavior undefined.  This can be used to open a FIFO for
   writing while there are no readers available.  A process that uses both ends of  the  con‐
   nection in order to communicate with itself should be very careful to avoid deadlocks.

翻译:
FIFO 特殊文件(命名管道)与管道类似,不同之处在于它是作为文件系统的一部分进行访问的。它可以由多个人打开 阅读或写作的过程。当进程通过 FIFO 交换数据时,内核会在内部传递所有数据而不写入数据 到文件系统。因此,FIFO 特殊文件在文件系统上没有内容;文件系统条目仅用作参考点 这样进程就可以使用文件系统中的名称访问管道。

内核为至少一个进程打开的每个 FIFO 特殊文件维护一个管道对象。 FIFO 必须打开于 在传递数据之前,两端(读和写)。通常,打开 FIFO 会阻塞,直到另一端也打开为止。

进程可以以非阻塞模式打开 FIFO。在这种情况下,即使尚未有人在写入端打开,打开只读也会成功 并且打开只写失败并出现 ENXIO(没有此类设备或地址),除非另一端已打开。

在 Linux 下,打开 FIFO 进行读取和写入在阻塞和非阻塞模式下都会成功。 POSIX 未定义此行为。 这可用于在没有可用读取器时打开 FIFO 进行写入。按顺序使用连接两端的进程 与自身沟通应该非常小心,以避免陷入僵局。

ctrl + z和ctrl + c的区别

以ctrl+z结束,将一个正在前台执行的命令放到后台,并且处于暂停状态,ctrl+z将任务中断,但是此任务并没有结束,他仍然在进程中。可以使用ps -aux查看你运行的read或者write进程是在后台的暂停状态,这个时候的管道的写段引用计数并不是等于零的,所以是阻塞的状态。可以使用ctrl + c强行中止当前的进程或者使用kill命令杀死进程。

有名管道实现简单版聊天

在这里插入图片描述

一读一写

chatA.c

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

int main() {
    // 1.判断有名管道文件是否存在
    int ret = access("fifo1", F_OK);
    if(ret == -1) {
        // 文件不存在
        printf("pipe not exists, creating fifo1 pipe...\n");
        int res = mkfifo("fifo1", 0664);
        if(res == -1) {
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2", F_OK);
    if(ret == -1) {
        // 文件不存在
        printf("pipe not exists, creating fifo2 pipe...\n");
        int res = mkfifo("fifo2", 0664);
        if(res == -1) {
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只写的方式打开管道fifo1
    int fdw = open("fifo1", O_WRONLY);
    if(fdw == -1) {
        perror("open");
        exit(0);
    }
    printf("open pipe fifo1 success! waiting for writing data...\n");

    // 3.以只读的方式打开管道fifo2
    int fdr = open("fifo2", O_RDONLY);
    if(fdr == -1) {
        perror("open");
        exit(0);
    }
    printf("open pipe fifo2 success! waiting for reading data...\n");

    char buf[128];
    // 4.循环的写读数据
    while(1) {
        memset(buf, 0, sizeof(buf));
        // 获取标准输入的数据
        fgets(buf, 128, stdin);
        // 写数据
        long int rett = write(fdw, buf, strlen(buf));
        if(rett == -1) {
            perror("write");
            break;
        }

        // 5.读管道数据
        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if(ret <= 0) {
            perror("read");
            break;
        }
        printf("chatB: %s\n", buf);
    }

    // 6.关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
}

chatB.c

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

int main() {
    // 1.判断有名管道文件是否存在
    int ret = access("fifo1", F_OK);
    if(ret == -1) {
        // 文件不存在
        printf("pipe not exists, creating fifo1 pipe...\n");
        int res = mkfifo("fifo1", 0664);
        if(res == -1) {
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2", F_OK);
    if(ret == -1) {
        // 文件不存在
        printf("pipe not exists, creating fifo2 pipe...\n");
        int res = mkfifo("fifo2", 0664);
        if(res == -1) {
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只读的方式打开管道fifo1
    int fdr = open("fifo1", O_RDONLY);
    if(fdr == -1) {
        perror("open");
        exit(0);
    }
    printf("open pipe fifo1 success! waiting for reading data...\n");

    // 3.以只写的方式打开管道fifo2
    int fdw = open("fifo2", O_WRONLY);
    if(fdw == -1) {
        perror("open");
        exit(0);
    }
    printf("open pipe fifo2 success! waiting for writing data...\n");

    char buf[128];
    // 4.循环的读写数据
    while(1) {
        // 5.读管道数据
        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if(ret <= 0) {
            perror("read");
            break;
        }
        printf("chatA: %s\n", buf);

        memset(buf, 0, sizeof(buf));
        // 获取标准输入的数据
        fgets(buf, 128, stdin);
        // 写数据
        long int rett = write(fdw, buf, strlen(buf));
        if(rett == -1) {
            perror("write");
            break;
        }
    }

    // 6.关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

一直读一直写模拟全双工通信

chatA2.c

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

int main()
{
    //  1.判断管道文件是否存在
    int ret = access("pipe2chat1", F_OK);
    if(ret == -1)
    {
        printf("pipe not exists, creating pipe2chat1 pipe...\n");

        ret = mkfifo("pipe2chat1", 0664);

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

    ret = access("pipe2chat2", F_OK);
    if(ret == -1)
    {
        printf("pipe not exists, creating pipe2chat2 pipe...\n");

        ret = mkfifo("pipe2chat2", 0664);

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

    // 2.只写方式打开管道1
    int fdw = open("pipe2chat1", O_WRONLY);
    if(fdw == -1)
    {
        perror("open");
        exit(0);
    }
    printf("open pipe pipe2chat1 success! waiting for writing data...\n");
    
    // 3.只读方式打管道2
    int fdr = open("pipe2chat2", O_RDONLY);
    if(fdr == -1)
    {
        perror("open");
        exit(0);
    }
    printf("open pipe pipe2chat2 success! waiting for reading data...\n");

    
    pid_t pid = fork();
    //父进程,写fifo1
    if(pid > 0)
    {
        char bufw[128];
        while(1)
        {
            memset(bufw, 0, 128);
            fgets(bufw, 128, stdin);
            int ret2 = write(fdw, bufw, strlen(bufw));
            if(ret2 == -1) {
                perror("write");
                exit(0);
            }
        }
    }
    //子进程,读fifo2
    else if(pid == 0)
    {
        char bufr[128];
        while (1)
        {
            memset(bufr, 0 ,128);
            int ret3 = read(fdr, bufr, 128);
            if(ret3 <= 0) {
                perror("read");
                break;
            }
            printf("chatB: %s\n", bufr);
        }
    }
    
    close(fdr);
    close(fdw);

    return 0;
}

chatB2.c

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

int main()
{
    //  1.判断管道文件是否存在
    int ret = access("pipe2chat1", F_OK);
    if(ret == -1)
    {
        printf("pipe not exists, creating pipe2chat1 pipe...\n");

        ret = mkfifo("pipe2chat1", 0664);

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

    ret = access("pipe2chat2", F_OK);
    if(ret == -1)
    {
        printf("pipe not exists, creating pipe2chat2 pipe...\n");

        ret = mkfifo("pipe2chat2", 0664);

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

    // 2.只读方式打开管道1
    int fdr = open("pipe2chat1", O_RDONLY);
    if(fdr == -1)
    {
        perror("open");
        exit(0);
    }
    printf("open pipe pipe2chat1 success! waiting for reading data...\n");
    
    // 3.只写方式打管道2
    int fdw = open("pipe2chat2", O_WRONLY);
    if(fdw == -1)
    {
        perror("open");
        exit(0);
    }
    printf("open pipe pipe2chat2 success! waiting for writing data...\n");
    
    pid_t pid = fork();
    //父进程,读fifo1
    if(pid > 0)
    {
        char bufr[128];
        while (1)
        {
            memset(bufr, 0 ,128);
            int ret3 = read(fdr, bufr, 128);
            if(ret3 <= 0) {
                perror("read");
                break;
            }
            printf("chatB: %s\n", bufr);
        }
    }
    //子进程,写fifo2
    else if(pid == 0)
    {
        char bufw[128];
        while(1)
        {
            memset(bufw, 0, 128);
            fgets(bufw, 128, stdin);
            int ret2 = write(fdw, bufw, strlen(bufw));
            if(ret2 == -1) {
                perror("write");
                exit(0);
            }
        }
    }
    
    close(fdr);
    close(fdw);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

一直读一直写模拟全双工通信完善版

chat3.c

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

int main() {
    int cpid = fork();
    if (cpid == -1) {
        perror("fork");
        exit(0);
    }

    // 子进程
    if (cpid == 0) {
        int fd;
        // 写fifo2
        if (access("fifo2", F_OK) == -1) {
            fd = mkfifo("fifo2", 0644);
        } else {
            fd = open("fifo2", O_WRONLY);
        }
        char buf[128];
        for (;;) {
            fgets(buf, 128, stdin);
            write(fd, buf, sizeof(buf));
        }
        close(fd);
    } else {
        // 父进程
        int fd;
        // 读fifo1
        if (access("fifo1", F_OK) == -1) {
            fd = mkfifo("fifo1", 0644);
        } else {
            fd = open("fifo1", O_RDONLY);
        }
        char buf[128];
        for (;;) {
        if (read(fd, buf, 128) <= 0) {
            break;
        }
        printf("chatB: %s\n", buf);
        }
        close(fd);
    }
    exit(0);
}

chat4.c

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

int main() {
    int cpid = fork();
    if (cpid == -1) {
        perror("fork");
    }

    // 子进程
    // 读fifo2
    if (cpid == 0) {
        int fd;
        if (access("fifo2", F_OK) == -1) {
            fd = mkfifo("fifo2", 0644);
        } else {
            fd = open("fifo2", O_RDONLY);
        }
        char buf[128];
        for (;;) {
            if (read(fd, buf, 128) <= 0) {
                break;
            }
            printf("chatA: %s\n", buf);
        }
        close(fd);
    } else {
        // 父进程
        int fd;
        // 写fifo1
        if (access("fifo1", F_OK) == -1) {
            fd = mkfifo("fifo1", 0644);
        } else {
            fd = open("fifo1", O_WRONLY);
        }
        char buf[128];
        for (;;) {
            fgets(buf, 128, stdin);
            write(fd, buf, strlen(buf) + 1);
        }
        close(fd);
    }

    exit(0);
}

在这里插入图片描述
在这里插入图片描述

内存映射

左边是虚拟地址空间,右边是物理磁盘上的文件

在这里插入图片描述
在这里插入图片描述

内存映射相关系统调用

在这里插入图片描述

mmap函数

man 2 mmap

在这里插入图片描述

test.txt

在这里插入图片描述

mmap-parent-child-ipc.c

/*
    #include <sys/mman.h>
    void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
        - 功能: 将一个文件或者设备的数据映射到内存中
        - 参数: 
            - void *addr: NULL, 由内核指定
            - length: 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
                    获取文件的长度: stat、lseek
            - prot: 对申请的内存映射区的操作权限
                -PROT_EXEC: 可执行的权限
                -PROT_READ: 读权限
                -PROT_WRITE: 写权限
                -PROT_NONE: 没有权限
                要操作映射内存, 必须要有读的权限
                PROT_READ、PROT_READ|PROT_WRITE
            - flags :
                - MAP_SHARED: 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
                - MAP_PRIVATE: 不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件. (copy on write)
            - fd: 需要映射的那个文件的文件描述符
                - 通过open得到, open的是一个磁盘文件
                - 注意: 文件的大小不能为0, open指定的权限不能和prot参数有冲突。The prot argument describes the desired memory protection of the  mapping  (and  must  not conflict with the open mode of the file).
                    prot: PROT_READ                open: 只读/读写 
                    prot: PROT_READ | PROT_WRITE   open: 读写
                    即prot的权限要小于open的权限, 但prot必须要由READ的权限
            - offset: 偏移量, 一般不用. 必须指定的是4k的整数倍, 0表示不偏移 
        - 返回值:
            成功返回创建的内存的首地址
            失败返回MAP_FAILED, (void *) -1
            On  success,  mmap() returns a pointer to the mapped area.  On error, the value MAP_FAILED(that is, (void *) -1) is returned, and errno is set to indicate the cause of the error.

    int munmap(void *addr, size_t length);
        - 功能: 释放内存映射
        - 参数: 
            - addr: 要释放的内存的首地址
            - length: 要释放的内存的大小, 要和mmap函数中的length参数的值一样 
*/

/*
    使用内存映射实现进程间通信:
    1.有关系的进程(父子进程)
        - 还没有子进程的时候
            - 通过唯一的父进程,先创建内存映射区
        - 有了内存映射区以后,创建子进程
        - 父子进程共享创建的内存映射区
    
    2.没有关系的进程间通信
        - 准备一个大小不是0的磁盘文件
        - 进程1 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 进程2 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 使用内存映射区通信

    注意: 内存映射区通信,是非阻塞。
*/

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

// 作业:使用内存映射实现没有关系的进程间的通信。
int main() {
    // 1. 打开一个文件
    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(0);
    }

    // 3. 创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        wait(NULL);
        // 父进程
        char buf[1024];
        strcpy(buf, (char *)ptr);
        printf("read data: %s\n", buf);
    } else if(pid == 0){
        // 子进程
        strcpy((char *)ptr, "happy birthday, zhushuang!!!");        
    }

    // 关闭内存映射区
    munmap(ptr, size);
    close(fd);

    return 0;
}

在这里插入图片描述
在这里插入图片描述
分页可以看成是内存分配的原子单元,对于程序员是透明的。文件比分页大小要小,程序也依然只是对文件大小的空间进行操作,而对于比文件大的那一部分不用做处理(那部分其实就是页内碎片,所以为了不浪费内存空间,用作内存映射的文件最好为分页大小的整数倍)

其实父子进程先mmap后fork,应该类似于malloc,父子进程共享一片存储空间,直接先malloc然后fork,父子进程都拥有malloc返回的地址

使用内存映射实现没有关系的进程间的通信

server.c

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

int main() {
    // 1. 打开一个文件
    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(0);
    }
    char buf[1024] = {0};
    while(1) {
        strcpy(buf, (char *)ptr);
        printf("read data: %s\n", buf);
        bzero(buf, 1024);
        sleep(1);
    }
    munmap(ptr, size);
    close(fd);
}

client.c

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

int main() {
    // 1. 打开一个文件
    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(0);
    }
    char buf[1024] = {0};
    while(1) {
        fgets(buf, 1024, stdin);
        strcpy((char*)ptr, buf);
        bzero(buf, 1024);
    }
    munmap(ptr, size);
    close(fd);
}

在这里插入图片描述
在这里插入图片描述

内存映射注意事项

在这里插入图片描述

1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(...);
ptr++;  可以对其进行++操作, 但不建议
munmap(ptr, len);   // 错误,要保存原先地址ptr

2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和prot参数的权限保持一致。

3.如果文件偏移量为1000会怎样?
偏移量必须是4K(就是4096)的整数倍,返回MAP_FAILED

4.mmap什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot
    - 只指定了写权限没有读权限
    - prot PROT_READ | PROT_WRITE
      第5个参数fd 通过open函数时指定的 O_RDONLY 或者 O_WRONLY

5.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
    - lseek()
    - truncate()

6.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("XXX");
mmap(,,,,fd,0);
close(fd); 
映射区还存在,创建映射区的fd被关闭,没有任何影响。

7.对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,,,,,);
4K
越界操作操作的是非法的内存 -> 段错误

在这里插入图片描述

使用内存映射实现文件拷贝的功能

memcpy函数

man 3 memcpy

在这里插入图片描述
在这里插入图片描述

copy.c

// 使用内存映射实现文件拷贝的功能
/*
    思路:
        1.对原始的文件进行内存映射
        2.创建一个新文件(拓展该文件)
        3.把新文件的数据映射到内存中
        4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
        5.释放资源
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    // 1.对原始的文件进行内存映射
    int fd = open("english.txt", O_RDWR);
    if(fd == -1) {
        perror("open");
        exit(0);
    }
  
    // 获取原始文件大小
    long int len = lseek(fd, 0, SEEK_END);

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

    // 对新创建的文件进行拓展
    truncate("cpy.txt", len);
    // 就是在内容开头添加空格, 但是整体还是只占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("ptr-mmap");
        exit(0);
    }

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

    // 4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
    memcpy(ptr1, ptr, len);

    // 释放资源: 谁先打开后释放, 谁后打开先释放
    munmap(ptr1, len);
    munmap(ptr, len);

    close(fd1);
    close(fd);

    return 0;
}

在这里插入图片描述

匿名映射: 不需要文件实体进程一个内存映射

man 2 mmap

在这里插入图片描述

mmap-anon.c

/*
    匿名映射: 不需要文件实体进程一个内存映射
*/

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

int main() {
    // 1.创建匿名内存映射区
    int len = 4096;
    // void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(ptr == MAP_FAILED) {
        perror("mmap");
        exit(0);
    }
    
    // 父子进程间通信
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        strcpy((char *)ptr, "happy birthday, zhushuang!");
        wait(NULL);
    } else if(pid == 0) {
        // 子进程
        sleep(1);
        printf("%s\n", (char *)ptr);
    }

    // 释放内存映射区
    int ret = munmap(ptr, len);
    if(ret == -1) {
        perror("munmap");
        exit(0);
    }

    return 0;
}

在这里插入图片描述
使用 truncate(“cpy.txt”, len); 就不需要 write 了,且 cpy.txt 和 english.txt 文件大小相同;

使用 lseek(fd1, len, SEEK_END); 只是修改了指针位置,因此需要 write(fd1, " ", 1); 才能真正改变文件大小,此时 cpy.txt 比 english.txt 大一个字节;

truncate后面有没有write都可以真正拓展文件

在这里插入图片描述
在这里插入图片描述
lseek后面不带write就会报错
在这里插入图片描述
在这里插入图片描述

信号概述

信号目的是用于通信,告诉进程发生了某种事情

在这里插入图片描述
在这里插入图片描述

kill -l

在这里插入图片描述

Linux信号一览表

在这里插入图片描述

SIGKILL:这里杀死任何进程指的是正常的进程哈,不包括僵尸进程

在这里插入图片描述

SIGSTOP表示暂停,不是停止

在这里插入图片描述
在这里插入图片描述

信号的5种默认处理动作

在这里插入图片描述

man 7 signal

在这里插入图片描述
在这里插入图片描述

从SIGUSR1开始对应多个数字,每个数字对应不同系统架构,我们基本是用中间这一列的

关于core文件

core.c

#include <string.h>
#include <stdio.h>

int main() {
    char* buf;
    strcpy(buf, "hw");
    return 0;
}

在这里插入图片描述

关于没有生成core文件这个问题,我的系统是ubuntu,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,查了之后发现ubuntu预装了apport错误收集系统,sudo service apport stop之后就可以了

在这里插入图片描述
Segmentation fault是段错误

kill、raise、abort函数

在这里插入图片描述

kill函数

int kill(pid_t pid, int sig);
    - 功能: 给任何的进程或者进程组pid, 发送任何的信号 sig
    - 参数:
        - pid: 需要发送给的进程的id
            > 0 : 将信号发送给指定的进程
            = 0 : 将信号发送给当前的进程组
            = -1 : 将信号发送给每一个有权限接收这个信号的进程
            < -1 : 这个pid=某个进程组的ID取反 (-12345)
        - sig: 需要发送的信号的编号或者是宏值, 0表示不发送任何信号

    kill(getppid(), 9);
    kill(getpid(), 9);
man 2 kill

在这里插入图片描述

raise函数

man 3 raise

在这里插入图片描述
kill.c

/*  
    #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)
            - sig: 需要发送的信号的编号或者是宏值, 0表示不发送任何信号

        kill(getppid(), 9);
        kill(getpid(), 9);
        
    int raise(int sig);
        - 功能:给当前进程或线程发送信号
        - 参数:
            - sig: 要发送的信号
        - 返回值:
            - 成功 0
            - 失败 非0
        相当于kill(getpid(), sig);   

    void abort(void);
        - 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
        相当于kill(getpid(), SIGABRT);
*/
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if(pid == 0) {
        // 子进程
        for(int i = 0; i < 5; i++) {
            printf("child process\n");
            sleep(1);
        }
    } else if(pid > 0) {
        // 父进程
        printf("parent process\n");
        sleep(2);
        printf("kill child process now\n");
        kill(pid, SIGINT);
    }

    return 0;
}

在这里插入图片描述

abort函数

void abort(void);
    - 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
    相当于kill(getpid(), SIGABRT);

alarm函数

man 2 alarm

在这里插入图片描述

alarm.c

/*
    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
        - 功能: 设置定时器(闹钟). 函数调用, 开始倒计时, 当倒计时为0的时候, 
                函数会给当前的进程发送一个信号: SIGALARM
        - 参数:
            seconds: 倒计时的时长, 单位: 秒。如果参数为0, 定时器无效(不进行倒计时, 不发信号)。
                    取消一个定时器, 通过alarm(0)。
        - 返回值:
            - 之前没有定时器, 返回0
            - 之前有定时器, 返回之前的定时器剩余的时间

    - SIGALARM:默认终止当前的进程, 每一个进程都有且只有唯一的一个定时器。也就是说多次调用alarm只会有最后一个生效
        alarm(10);  -> 返回0
        过了1秒
        alarm(5);   -> 返回9
        In any event any previously set alarm() is canceled.

    alarm(100) -> 该函数是不阻塞的
*/
#include <stdio.h>
#include <unistd.h>

int main() {
    int seconds = alarm(5);
    printf("seconds = %d\n", seconds);

    sleep(2);

    seconds = alarm(3);  // 不阻塞
    printf("seconds = %d\n", seconds);

    while(1) {

    }

    return 0;
}

在这里插入图片描述

alarm的定时时间包含的是:用户+系统内核的运行时间

alarm1.c

// 1秒钟电脑能数多少个数?
#include <stdio.h>
#include <unistd.h>

/*
    实际的时间 = 内核时间(系统调用) + 用户时间(普通代码执行)
    进行文件IO操作的时候比较浪费时间

    定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/
int main() {
    alarm(1);

    int i = 0;
    while(1) {
        printf("%i\n", i++);
    }

    return 0;
}

在这里插入图片描述

setitimer定时器函数

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
    - 功能:设置定时器(闹钟)。可以替代alarm函数(单位是秒)。精度微秒us, 可以实现周期性定时
    - 参数:
        - which: 定时器以什么时间计时
          ITIMER_REAL: 真实时间, 时间到达, 发送 SIGALRM   常用
          ITIMER_VIRTUAL: 用户时间, 时间到达, 发送 SIGVTALRM
          ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算, 时间到达, 发送 SIGPROF

        - new_value: 设置定时器的属性
        
            struct itimerval {           // 定时器的结构体
            struct timeval it_interval;  // 周期性定时器的时间间隔
            struct timeval it_value;     // 距下次到期时间
            };

            struct timeval {        // 时间的结构体
                time_t      tv_sec;     //  秒数     
                suseconds_t tv_usec;    //  微秒    
            };

        过10秒后, 每个2秒定时一次
       
        - old_value: 记录上一次的定时的时间参数, 一般不使用, 指定NULL
    
    - 返回值:
        成功 0
        失败 -1 并设置错误号
man 2 setitimer

在这里插入图片描述

这些系统调用提供对间隔计时器的访问,即最初在未来某个时间点到期的计时器,并且(可选)此后定期到期。当计时器到期时,将为调用进程生成一个信号,并将计时器重置为指定的时间间隔(如果时间间隔非零)。

提供了三种类型的计时器(通过 which 参数指定),每种计时器针对不同的时钟进行计数,并在计时器到期时生成不同的信号

在这里插入图片描述

ITIMER_VIRTUAL: 该计时器针对进程消耗的用户模式 ​​CPU 时间进行倒计时。 (该测量值包括进程中所有线程消耗的 CPU 时间。)每次到期时,都会生成一个 SIGVTALRM 信号。

ITIMER_PROF: 该计时器针对进程消耗的总(即用户和系统)CPU 时间进行倒计时。 (该测量包括进程中所有线程消耗的 CPU 时间。)每次到期时,都会生成一个 SIGPROF 信号。

setitimer.c

/*
    #include <sys/time.h>
    int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
        - 功能:设置定时器(闹钟)。可以替代alarm函数(单位是秒)。精度微秒us, 可以实现周期性定时
        - 参数:
            - which: 定时器以什么时间计时
              ITIMER_REAL: 真实时间, 时间到达, 发送 SIGALRM   常用
              ITIMER_VIRTUAL: 用户时间, 时间到达, 发送 SIGVTALRM
              ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算, 时间到达, 发送 SIGPROF

            - new_value: 设置定时器的属性
            
                struct itimerval {           // 定时器的结构体
                struct timeval it_interval;  // 周期性定时器的时间间隔
                struct timeval it_value;     // 距下次到期时间
                };

                struct timeval {        // 时间的结构体
                    time_t      tv_sec;     //  秒数     
                    suseconds_t tv_usec;    //  微秒    
                };

            过10秒后, 每个2秒定时一次
           
            - old_value: 记录上一次的定时的时间参数, 一般不使用, 指定NULL
        
        - 返回值:
            成功 0
            失败 -1 并设置错误号
*/

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>

// 过3秒以后, 每隔2秒钟定时一次
int main() {
    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    // 非阻塞的
    int ret = setitimer(ITIMER_REAL, &new_value, NULL);
    printf("setitimer start...\n");

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

    getchar();

    return 0;
}

在这里插入图片描述

signal信号捕捉函数

在这里插入图片描述

signal函数

man 2 signal

在这里插入图片描述
signal.c

/*
    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
        - 功能: 置某个信号的捕捉行为
        - 参数:             
            - signum: 要捕捉的信号
            - handler: 捕捉到信号要如何处理
                - SIG_IGN: 忽略信号
                - SIG_DFL: 使用信号默认的行为(相当于没有捕捉)
                - 回调函数:  这个函数是内核调用, 程序员只负责写, 捕捉到信号后如何去处理信号。
                回调函数:                     
                    - 需要程序员实现, 提前准备好的, 函数的类型根据实际需求, 看函数指针的定义
                    - 不是程序员调用, 而是当信号产生, 由内核调用
                    - 函数指针是实现回调的手段, 函数实现之后, 将函数名放到函数指针的位置就可以了。

        - 返回值:             
            成功, 返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败, 返回SIG_ERR, 设置错误号
            
    SIGKILL SIGSTOP不能被捕捉, 不能被忽略 
*/

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

// 过3秒以后, 每隔2秒钟定时一次
int main() {
    // 注册信号捕捉
    signal(SIGALRM, SIG_IGN);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    // 非阻塞的
    int ret = setitimer(ITIMER_REAL, &new_value, NULL);
    printf("setitimer start...\n");

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

    // 暂停程序不让其退出
    getchar();

    return 0;
}

在这里插入图片描述
signal.c

/*
    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
        - 功能: 置某个信号的捕捉行为
        - 参数:             
            - signum: 要捕捉的信号
            - handler: 捕捉到信号要如何处理
                - SIG_IGN: 忽略信号
                - SIG_DFL: 使用信号默认的行为(相当于没有捕捉)
                - 回调函数:  这个函数是内核调用, 程序员只负责写, 捕捉到信号后如何去处理信号。
                回调函数:                     
                    - 需要程序员实现, 提前准备好的, 函数的类型根据实际需求, 看函数指针的定义
                    - 不是程序员调用, 而是当信号产生, 由内核调用
                    - 函数指针是实现回调的手段, 函数实现之后, 将函数名放到函数指针的位置就可以了。

        - 返回值:             
            成功, 返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败, 返回SIG_ERR, 设置错误号
            
    SIGKILL SIGSTOP不能被捕捉, 不能被忽略 
*/

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

// 过3秒以后, 每隔2秒钟定时一次
int main() {
    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    signal(SIGALRM, SIG_DFL);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    // 非阻塞的
    int ret = setitimer(ITIMER_REAL, &new_value, NULL);
    printf("setitimer start...\n");

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

    // 暂停程序不让其退出
    getchar();

    return 0;
}

在这里插入图片描述
signal.c

/*
    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
        - 功能: 置某个信号的捕捉行为
        - 参数:             
            - signum: 要捕捉的信号
            - handler: 捕捉到信号要如何处理
                - SIG_IGN: 忽略信号
                - SIG_DFL: 使用信号默认的行为(相当于没有捕捉)
                - 回调函数:  这个函数是内核调用, 程序员只负责写, 捕捉到信号后如何去处理信号。
                回调函数:                     
                    - 需要程序员实现, 提前准备好的, 函数的类型根据实际需求, 看函数指针的定义
                    - 不是程序员调用, 而是当信号产生, 由内核调用
                    - 函数指针是实现回调的手段, 函数实现之后, 将函数名放到函数指针的位置就可以了。

        - 返回值:             
            成功, 返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败, 返回SIG_ERR, 设置错误号
            
    SIGKILL SIGSTOP不能被捕捉, 不能被忽略 
*/

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num) {
    printf("captured signal's number: %d\n", num);
    printf("----------------------------------\n");
}

// 过3秒以后, 每隔2秒钟定时一次
int main() {
    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    // signal(SIGALRM, SIG_DFL);
    // void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值
    __sighandler_t rett = signal(SIGALRM, myalarm);
    if(rett == SIG_ERR) {
        perror("signal");
        exit(0);
    }

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    // 非阻塞的
    int ret = setitimer(ITIMER_REAL, &new_value, NULL);
    printf("setitimer start...\n");

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

    // 暂停程序不让其退出
    getchar();

    return 0;
}

在这里插入图片描述

信号集及相关函数

在这里插入图片描述

最左边是虚拟地址空间,PCB里有未决信号集和阻塞信号集

在这里插入图片描述

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

2.信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
- SIGINT信号状态被存储在第二个标志位上
    - 这个标志位的值为0, 说明信号不是未决状态
    - 这个标志位的值为1, 说明信号处于未决状态

3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
- 阻塞信号集默认不阻塞任何的信号
- 如果想要阻塞某些信号需要用户调用系统的API

4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
- 如果没有阻塞,这个信号就被处理
- 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

信号集相关的函数

可以这样理解,内核手上有一个小本本,叫信号集,上面记录着哪些信号要被阻塞。如果我们想要控制,就自己写一个小本本,然后通过API函数,将这个小本本交给内核。内核就会按照我们的要求阻塞信号。

这些函数是对我们自定义的信号集进行操作

在这里插入图片描述

sigemptyset函数

man 3 sigemptyset

在这里插入图片描述

sigset.c

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

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

    int sigfillset(sigset_t *set);
        - 功能: 将信号集中的所有的标志位置为1
        - 参数: set, 传出参数, 需要操作的信号集
        - 返回值: 成功返回0,  失败返回-1

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

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

    int sigismember(const sigset_t *set, int signum);
        - 功能: 判断某个信号是否阻塞
        - 参数: 
            - set: 需要操作的信号集
            - signum: 需要判断的那个信号
        - 返回值: 
            1 : signum被阻塞
            0 : signum不阻塞
            -1 :  失败

*/

#include <signal.h>
#include <stdio.h>

int main() {
    // 创建一个信号集
    sigset_t set;

    // 清空信号集的内容
    sigemptyset(&set);

    // 判断 SIGINT 是否在信号集 set 里
    int ret = sigismember(&set, SIGINT);
    if(ret == 0) {
        printf("SIGINT is not blocked\n");
    } else if(ret == 1) {
        printf("SIGINT is blocked\n");
    }

    // 添加几个信号到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 判断 SIGINT 是否在信号集 set 里
    ret = sigismember(&set, SIGINT);
    if(ret == 0) {
        printf("SIGINT is not blocked\n");
    } else if(ret == 1) {
        printf("SIGINT is blocked\n");
    }

    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT is not blocked\n");
    } else if(ret == 1) {
        printf("SIGQUIT is blocked\n");
    }

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);

    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT is not blocked\n");
    } else if(ret == 1) {
        printf("SIGQUIT is blocked\n");
    }
    
    return 0;
}

在这里插入图片描述

sigprocmask函数使用

在这里插入图片描述
在这里插入图片描述

man 2 sigprocmask

在这里插入图片描述
SIG_UNBLOCK参数作用: mask &= ~set
在这里插入图片描述

sigpending函数

man 2 sigpending

在这里插入图片描述

小案例——把所有的常规信号(1-31)的未决状态打印到屏幕

sigprocmask.c

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

    int sigpending(sigset_t *set);
        - 功能: 获取内核中的未决信号集
        - 参数: set, 传出参数, 保存的是内核中的未决信号集中的信息 
        - 返回值:
            成功返回0
            失败返回-1
            sigpending() returns 0 on success and -1 on error.  In the event of an error, errno is set to indicate the cause.
*/

// 编写一个程序, 把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的, 通过键盘产生这些信号

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

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) {
        num++;
        // 获取当前的未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);

        // 遍历前32位
        for(int i = 1; i <= 31; i++) {
            if(sigismember(&pendingset, i) == 1) {
                printf("1");
            } else if(sigismember(&pendingset, i) == 0) {
                printf("0");
            } else {
                perror("sigismember");
                exit(0);
            }
        }
        printf("\n");
        sleep(1);
        if(num == 80) {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
    }

    return 0;
}

在这里插入图片描述

如何将&后台运行程序转移至前台,用命令fg即可

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

sigaction信号捕捉函数

int sigaction(int signum, const struct sigaction *act,
                        struct sigaction *oldact);

    - 功能: 检查或者改变信号的处理。信号捕捉
    - 参数: 
        - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
        - act : 捕捉到信号之后的处理动作
        - oldact : 上一次对信号捕捉相关的设置, 一般不使用, 传递NULL
    - 返回值: 
        成功 0
        失败 -1
        sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error

 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;
    // 被废弃掉了
    void     (*sa_restorer)(void);
};

在这里插入图片描述

man 2 sigaction

在这里插入图片描述
在这里插入图片描述

sigaction.c

/*
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act,
                            struct sigaction *oldact);

        - 功能: 检查或者改变信号的处理。信号捕捉
        - 参数: 
            - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
            - act : 捕捉到信号之后的处理动作
            - oldact : 上一次对信号捕捉相关的设置, 一般不使用, 传递NULL
        - 返回值: 
            成功 0
            失败 -1
            sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error

     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;
        // 被废弃掉了
        void     (*sa_restorer)(void);
    };

*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num) {
    printf("captured signal's number: %d\n", num);
    printf("----------------------------------\n");
}

// 过3秒以后, 每隔2秒钟定时一次
int main() {
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;

    // 清空临时阻塞信号集
    sigemptyset(&act.sa_mask);

    // 注册信号捕捉
    sigaction(SIGALRM, &act, NULL);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    // 非阻塞的
    int ret = setitimer(ITIMER_REAL, &new_value, NULL);
    printf("setitimer start...\n");

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

    // 暂停程序不让其退出
    int res = getchar();
    if(res == -1) {
        perror("getchar");
        exit(0);
    }

    return 0;
}

在这里插入图片描述

使用sigaction信号捕捉函数问题

在这里插入图片描述

信号处理程序中断系统调用和库函数

如果在系统调用或库函数调用被阻塞时,调用信号处理程序,则:

  • 信号处理程序返回后自动重新启动调用;
  • 调用失败并出现错误 EINTR。

发生这两种行为中的哪一种取决于接口以及是否使用 SA_RESTART 标志建立信号处理程序(请参阅 sigaction(2))。不同 UNIX 系统的细节有所不同;下面是 Linux 的详细信息。如果对以下接口之一的阻塞调用被信号处理程序中断,则在使用 SA_RESTART 标志的情况下,该调用将在信号处理程序返回后自动重新启动;否则调用失败并出现错误 EINTR:

在这里插入图片描述

修改代码

sigaction.c

/*
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act,
                            struct sigaction *oldact);

        - 功能: 检查或者改变信号的处理。信号捕捉
        - 参数: 
            - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
            - act : 捕捉到信号之后的处理动作
            - oldact : 上一次对信号捕捉相关的设置, 一般不使用, 传递NULL
        - 返回值: 
            成功 0
            失败 -1
            sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error

     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;
        // 被废弃掉了
        void     (*sa_restorer)(void);
    };

*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num) {
    printf("captured signal's number: %d\n", num);
    printf("----------------------------------\n");
}

// 过3秒以后, 每隔2秒钟定时一次
int main() {
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;

    // 清空临时阻塞信号集
    sigemptyset(&act.sa_mask);

    // 注册信号捕捉
    sigaction(SIGALRM, &act, NULL);

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    // 非阻塞的
    int ret = setitimer(ITIMER_REAL, &new_value, NULL);
    printf("setitimer start...\n");

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

    while(1);

    return 0;
}

在这里插入图片描述

内核实现信号捕捉的过程

在这里插入图片描述

sa_mask如果不清空,它刚创建出来,里面的数据是随机的,所以我们要清空。清空了就是默认不临时阻塞任何的信号。可以设置其他的临时阻塞信号,那么当产生这个信号时不会执行信号捕捉函数,而等这个信号捕捉函数执行完了,才会不阻塞

SIGCHLD信号

在这里插入图片描述

sigchld.c

/*
    SIGCHLD信号产生的3个条件:
        1.子进程结束
        2.子进程暂停了
        3.子进程继续运行
        都会给父进程发送该信号, 父进程默认忽略该信号 
    
    使用SIGCHLD信号解决僵尸进程的问题 
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>

void myFun(int num) {
    printf("captured signal's number: %d\n", num);
    // 回收子进程PCB的资源
    // 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_flags = 0;
        act.sa_handler = myFun;
        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;
}

在这里插入图片描述

总结

信号阻塞之后不能被捕捉

因为该方法就是为了处理子进程终止的太早了。假设极端情况下,20个子进程老早就终止了,内核收到SIGCHLD信号,会将未决信号集中的17号位置为1(也只有1次),但同时该信号被阻塞,所以该17号位置保持1,等待处理。当注册完信号捕捉以后,再解除阻塞。内核发现此时第17号位居然是1,那就去执行对应的处理函数。在处理函数中,waitpid函数发现:“哎呦,这怎么躺着20具僵尸呀”,然后它就循环了20次,回收了这20具僵尸,皆大欢喜~

waitpid在回收子进程的时候和子进程发送信号是独立的,waitpid并不是靠信号去驱动回收进程的。比如有一个子进程死亡,此时有一个僵尸进程,同时发送信号,被接收了,然后在myFun里面执行时,又有19个子进程死亡,那么又产生了19个僵尸进程,同时发送信号;此时waitpid在回收的时候只是检查是否有僵尸进程,则此时如果有僵尸进程则一直回收就行,不用去等信号。

如果从开始注册信号到注册成功这段时间里,有n个SIGCHID信号产生的话,那么第一个产生的SIGCHID会抢先将未决位置为1,余下的n-1个SIGCHID被丢弃,然后当阻塞解除之后,信号处理函数发现这时候对应信号的未决位为1,继而执行函数处理该信号,处理函数中的while循环顺带将其他n-1子进程也一网打尽了,在这期间未决位的状态只经历了两次变化,即0->1->0

QA

在这里插入图片描述
信号产生,内核中未决信号集SIGCHLD信号置1,内核调用信号捕捉函数myfun的同时把该信号置0,也就是说进入myfun函数后,内核依然是可以接收到SIGCHLD信号的。但是Linux为了防止某一个信号重复产生,在myfun函数进行多次递归导致堆栈空间爆了,它在调用myfhun函数会自动(内核自己完成)堵塞同类型信号。当然也可以用参数,自己指定要堵塞其他类型的信号。要注意的是,这里堵塞不是不接收信号,而是接收了不处理。当myfun函数结束,堵塞就会自动解除,该信号会传递给父进程。想象一个场景,20个子进程,先瞬间终止10个,父进程捕获到信号,进入myfun函数wait回收。这里有个点就是,父进程在执行myfun函数的时候,其他子进程不是挂起的,也是会运行的,至于怎么调度,那就看神秘莫测的调度算法了。在回收过程中,其余10个子进程也终止了,发出呼喊:“爹,快来回收我!”。父进程:“我没空,我还在myfun函数中干活”。于是内核将未决集中SIGCHLD信号置1等待处理,父进程在myfun函数中使用waitpid函数回收僵尸,”怎么越回收越多呀”,在while函数的加持下,他成功回收了20个僵尸。当它回到主函数打算休息下,内核叮的一声,有你的SIGCHLD信号,父进程以为有僵尸再次进入myfun函数,执行waipid函数,发现压根没有僵尸(上一次都回收完了),甚至儿子都没了(返回-1,break),骂骂咧咧返回了主函数。这就是为什么父进程捕获到了信号,进入了myfun函数,一个僵尸都没回收的真相。

段错误

在这里插入图片描述

printf函数是不可重入函数

段错误是个迷,有的人碰到过几次,有的人怎么也碰不到,这是由于神秘莫测的调度算法导致的。【潇潇_暮雨】小伙伴提出了,这是调用了不可重入的函数。《Linux/UNIX系统编程手册》第21.1.2节 对可重入函数进行了详细的解释,有兴趣的可以去翻一下。

可重入函数的意思是:函数由两条或多条线程调用时,即便是交叉执行,其效果也与各线程以未定义顺序依次调用时一致。通俗点讲,就是存在一个函数,A线程执行一半,B线程抢过CPU又来调用该函数,执行到1/4倍A线程抢回执行权。在这样不断来回执行中,不出问题的,就是可重入函数。多线程中每个线程都有自己的堆栈,所以如果函数中只用到局部变量肯定是可重入的,没问题的。但是更新了全局变量或静态数据结构的函数可能是不可重入的。假设某线程正在为一个链表结构添加一个新的链表项,而另外一个线程也视图更新同一链表。由于中间涉及多个指针,一旦另一线程中断这些步骤并修改了相同指针,结果就会产生混乱。但是并不是一定会出现,一定是A线程刚好在修改指针,另外一线程又去修改才会出现。这就是为什么该问题复现难度较高的原因。

作者在文中指出,将静态数据结构用于内部记账的函数也是不可重入的。其中最明显的例子就是stdio函数库成员(printf()、scanf()等),它们会为缓冲区I/O更新内部数据结构。所以,如果在捕捉信号处理函数中调用了printf(),而主程序又在调用printf()或其他stdio函数期间遭到了捕捉信号处理函数的中断,那么有时就会看到奇怪的输出,设置导致程序崩溃。虽然printf()不是异步信号安全函数,但却频频出现在各种示例中,是因为在展示对捕捉信号处理函数的调用,以及显示函数中相关变量的内容时,printf()都不失为一种简单而又便捷的方式。真正的应用程序应当避免使用该类函数。

printf函数会使用到一块缓冲区,这块缓冲区是使用malloc或类似函数分配的一块静态内存。所以它是不可重入函数。

共享内存

共享内存比内存映射效率高一些,因为内存映射里面需要管理文件,通过文件做进程间的通信

IPC即进程间通信
在这里插入图片描述
在这里插入图片描述

共享内存使用步骤

在这里插入图片描述
在这里插入图片描述

共享内存操作函数

在这里插入图片描述

shmget函数

man 2 shmget

在这里插入图片描述
shmget() 返回与参数键的值关联的 System V 共享内存段的标识符。 如果 key 的值为 IPC_PRIVATE 或 key 不是 IPC_PRIVATE,不存在与 key 对应的共享内存段,并且指定了 IPC_CREAT,则创建一个新的共享内存段,其大小等于 size 的值向上舍入到 PAGE_SIZE 的倍数 在 shmflg 中。

除了上述标志之外,shmflg 的最低有效 9 位指定授予所有者、组和其他人的权限。这些位与 open(2) 的模式参数具有相同的格式和相同的含义。目前,系统不使用执行权限

在这里插入图片描述

共享内存相关的函数
#include <sys/ipc.h>
#include <sys/shm.h>

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

shmat函数

man 2 shmat

在这里插入图片描述
Attach the segment for read-only access. The process must have read permission for the segment. If this flag is not specified, the segment is attached for read and write access, and the process must have read and write permission for the segment. There is no notion of a write-only shared memory segment

附加该段以进行只读访问。该进程必须具有该段的读取权限。如果未指定此标志,则附加该段以进行读写访问,并且进程必须具有该段的读写权限。不存在只写共享内存段的概念

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

shmdt函数

int shmdt(const void *shmaddr);
- 功能:解除当前进程和共享内存的关联
- 参数:
    shmaddr:共享内存的首地址
- 返回值:成功 0, 失败 -1

shmctl函数

man 2 shmctl

在这里插入图片描述
在这里插入图片描述

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

共享内存小案例

write_shm.c

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

int main() {
    // 1.创建一个共享内存
    int shmid = shmget(100, 4096, IPC_CREAT | 0664);
    printf("shmid : %d\n", shmid);

    // 2.和当前进程进行关联
    void * ptr = shmat(shmid, NULL, 0);
    char * str = "happy birthday zhushuang!";
    printf("sizeof str is : %ld\n", sizeof(str));

    // 3.写数据
    memcpy(ptr, str, strlen(str) + 1);

    printf("press any key to continue\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

read_shm.c

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

int main() {
    // 1.获取一个共享内存
    int shmid = shmget(100, 0, IPC_CREAT);
    printf("shmid : %d\n", shmid);

    // 2.和当前进程进行关联
    void * ptr = shmat(shmid, NULL, 0);

    // 3.读数据
    puts((char*)ptr);

    printf("press any key to continue\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

sizeof 和 strlen的区别

在这里插入图片描述
在这里插入图片描述

ftok函数

man 3 ftok

在这里插入图片描述

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

总结

问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
- shm_nattach 记录了关联的进程个数

共享内存操作命令

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


问题2:可不可以对共享内存进行多次删除 shmctl
- 可以的
- 因为shmctl 标记删除共享内存,不是直接删除
- 什么时候真正删除呢?
    当和共享内存关联的进程数为0的时候,就真正被删除
- 当共享内存的key为0的时候,表示共享内存被标记删除了
    如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能再次进行关联。

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

1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高
3.内存
    所有的进程操作的是同一块共享内存。
    内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全
    - 进程突然退出
        共享内存还存在
        内存映射区消失
    - 运行进程的电脑死机,宕机了
       存储在共享内存中的数据就没有了
       内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。

5.生命周期
    - 内存映射区:进程退出,那么内存映射区就销毁
    - 共享内存:进程退出,共享内存还在,我们需要手动标记删除(所有的关联的进程数为0),或者关机才能把它销毁
        如果一个进程退出,会自动和共享内存进行取消关联。

如果此时共享内存段的键变为了0,那如果现在重新让一个进程关联标识为100(即0x64)的共享内存段,就不会关联到

当共享内存段被IPC_RMID标记删除后,key值变会为0。此时再开一个新进程(./read)进行关联shmget函数会返回-1,进程会被终止

删除共享内存的时候,并不会立即删除,读和写的操作依然可以进行
在这里插入图片描述
在这里插入图片描述
只是标识符发生了变化,表示不能进行新的映射连接,等到映射连接数变为0的时候,这块儿共享内存就会被删除

守护进程

终端

在这里插入图片描述

echo $$可以查看当前终端对应的进程号

在这里插入图片描述
在这里插入图片描述

进程组

有时我们称进程组为shell作业
在这里插入图片描述

会话

在这里插入图片描述

进程组、会话、控制终端之间的关系

关于find和wc为什么父进程是bash(400),但是进程组ID确是658,而不是复制父进程的组ID。其实fork函数产生的子进程才是复制父进程的组ID。那通过这种命令产生的子进程,组ID是怎么确立的?在《Linux/UNIX系统编程手册》2.13 中提到,“shell执行的每个程序都会在一个新进程内发起”,这句话解释了为什么find、wc、sort、uniq这四个都是一个单独的进程。“除了Bourne shell以外,几乎所有主流shell都提供了一种交互式特性,名为任务控制。该特性允许用户同时执行并操纵多条命令或管道。在支持任务控制的shell中,会将管道内所有进程置于一个新进程组或任务中。如果情况很简单,shell命令行只包含一条命令,那么就会创建一个只包含单个进程的新进程组。进程组中每个进程都具有相同的进程组标识符,其实就是进程组组长的ID”,这段话可以解释为什么两条命令产生了两个新的进程组,并且不同于bash进程组。这种shell命令创建子进程一定要和fork函数区分开来。
在这里插入图片描述

在这里插入图片描述

进程组、会话操作函数

在这里插入图片描述

守护进程概念

在这里插入图片描述

守护进程的创建步骤

在这里插入图片描述
在这里插入图片描述

创建新的会话,就是为了使守护进程脱离控制终端。

在这里插入图片描述

localtime函数

man 3 localtime

在这里插入图片描述

asctime函数

man 3 asctime

在这里插入图片描述

setsid函数

man 2 setsid

在这里插入图片描述

守护进程小案例

daemon.c

/*
    写一个守护进程, 每隔2s获取一下系统时间, 将这个时间写入到磁盘文件中 
*/

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

void work(int num) {
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm *loc = localtime(&tm);
    char *str = asctime(loc);
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd, str, strlen(str));
    close(fd);
}

int main() {
    // 1.创建子进程,退出父进程
    pid_t pid = fork();

    if(pid > 0) {
        exit(0);
    }

    // 2.将子进程重新创建一个会话
    setsid();

    // 3.设置掩码
    umask(022);

    // 4.更改工作目录
    chdir("/home/ubuntu");

    // 5. 关闭、重定向文件描述符
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 6.业务逻辑

    // 捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    // 清空临时阻塞信号集
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;
    // 设置间隔的时间
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 设置延迟的时间,2秒之后开始第一次定时
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;

    // 创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 不让进程结束
    while(1) {
        sleep(10);
    }

    return 0;
}

kill是发送信号的shell命令,可以给指定进程发送指定信号。不能被控制终端的Ctrl+C或者Ctrl+\结束,因为它没有自己的控制终端,是新的会话没有绑定控制终端,但它还是一个进程,可以接收信号。
在这里插入图片描述
在这里插入图片描述

小技巧:在文件里输入:e可以刷新文件内容

在这里插入图片描述
在这里插入图片描述

tail -f time.txt也可以刷新文件

在这里插入图片描述
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!