Linux 多进程开发

0、程序和进程

程序 是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程。
进程 是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。

1、区别
程序是静态的,进程是动态的,程序是存储在某种介质上的二进制代码,进程对应了程序的执行过程,系统不需要为一个不执行的程序创建进程,一旦进程被创建,就处于不断变化的动态过程中,对应了一个不断变化的上下文环境。

2、联系
进程是程序的一次执行,而进程总是对应至少一个特定的程序。一个程序可以对应多个进程,同一个程序可以在不同的数据集合上运行,因而构成若干个不同的进程。几个进程能并发地执行相同的程序代码,而同一个进程能顺序地执行几个程序。

关于进程和程序的区别,《现代操作系统》中用了一个比喻形象说明:一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需要的原料,在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法),计算机科学家就是处理机(CPU),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱,取来各种原料以及烘制蛋糕等一系列动作的总和。

1、并行和并发

在这里插入图片描述

2、进程控制块(PCB)—— 进程信息管理

为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个
PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是
task_struct 结构体。
在 /usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以查看 struct task_struct 结构体定义。
其内部成员有很多,我们只需要掌握以下部分即可:

  • 进程id:系统中每个进程有唯一的 id,用 pid_t 类型表示,其实就是一个非负整数
  • 进程的状态:有就绪、运行、挂起、停止等状态
  • 进程切换时需要保存和恢复的一些CPU寄存器
  • 描述虚拟地址空间的信息
  • 描述控制终端的信息
  • 当前工作目录(Current Working Directory)
  • umask 掩码
  • 文件描述符表,包含很多指向 file 结构体的指针
  • 和信号相关的信息
  • 用户 id 和组 id
  • 会话(Session)和进程组
  • 进程可以使用的资源上限(Resource Limit)

3、进程的状态

进程状态转换

进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。

  • 运行态:进程占有处理器正在运行;
  • 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常 将它们排成一个队列,称为就绪队列;
  • 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成;
    在这里插入图片描述

进程处于阻塞态,CPU的资源会释放掉,调度程序调度给别的程序执行。
程序运行时与用户交互(控制台等待输入),调用的就是阻塞函数。
阻塞态无法直接变为运行态,首先转换为就绪态,然后被调用运行(抢CPU资源)才能转为运行态,因为阻塞时会把CPU资源释放掉。

  • 新建态:进程刚被创建时的状态,尚未进入就绪队列;
  • 终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终
    止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待
    善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。
    在这里插入图片描述

就绪态可以直接到终止态。
程序终止后,虚拟地址空间中用户区的数据被释放,内核区的数据还保留在系统中,等待善后。

进程相关的命令

ps aux / ajx
a:显示终端上的所有进程,包括其他用户的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:列出与作业控制相关的信息

1、ps aux

ps aux      显示当前进程的一个快照(非动态)

在这里插入图片描述

  • TTY是进程所属的终端,比如ps aux这个进程属于 pts/3 终端。
  • STAT是状态,比如ps aux就是R状态,即正在运行。
  • COMMAND是命令,比如ps aux。

在这里插入图片描述
2、ps ajx

ps ajx     显示当前进程的一个快照(非动态)

在这里插入图片描述
PID是进程ID(学生)
PPID是父进程ID
PGID是进程组ID,一个组里有多个进程(教室)
SID是会话ID,一个会话有多个组(学校)

3、top

top            动态的显示进程信息

可以在使用 top 命令时加上 -d 来指定显示信息更新的时间间隔,在 top 命令执行后,可以按以下按键对显示的结果进行排序:

  • M 根据内存使用量排序
  • P 根据 CPU 占有率排序
  • T 根据进程运行时间长短排序
  • U 根据用户名来筛选进程
  • K 输入指定的 PID 杀死进程

在这里插入图片描述
4、kill
杀死进程(kill名并不是去杀死一个进程,而是给进程发送某个信号)

kill [-signal] pid
kill –l 列出所有信号
kill –SIGKILL 进程ID
kill -9 进程ID
killall name 根据进程名杀死进程

在这里插入图片描述

进程号相关函数

1、每个进程都由进程号来标识,其类型为 pid_t(整型),进程号的范围:0~32767。进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。

2、任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)
在这里插入图片描述

  • 任何进程都有父进程,比如 a.out 的父进程就是 终端

3、进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号。

进程号和进程组的获取函数:

pid_t getpid(void); //当前进程ID
pid_t getppid(void); //当前进程父进程ID
pid_t getpgid(pid_t pid); //当前进程进程组的ID

4、进程创建 —— fork函数

系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成 进程树结构模型
fork() 通过复制调用进程来创建一个新进程。 新进程被称为子进程。 调用进程称为父进程。
fork函数,意译为分叉。

man 2 fork

在这里插入图片描述

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
函数的作用:用于创建子进程。
        返回值:
            fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。
            如何区分父进程和子进程:通过fork的返回值。

返回值:

  • 成功:返回两次,在子进程中返回 0,在父进程中返回子进程 ID
  • 失败:在父进程中返回-1,表示创建子进程失败,并且设置errno

失败的两个主要原因:

  • 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN
  • 系统内存不足,这时 errno 的值被设置为 ENOMEM

测试一下:

// 创建子进程
    pid_t pid = fork(); //此时会分叉

    // 判断是父进程还是子进程
    if (pid > 0){
        printf("pid : %d\n", pid);
        // 如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("im parent process, pid: %d, ppid: %d\n", getpid(), getppid());
    } else if (pid == 0){
        // 如果等于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);
    }

输出:

petri@XX:~/lesson02/01$ ./fork
//因为fork分叉,执行了两组代码:

//父进程部分
pid : 575
im parent process, pid: 574, ppid: 27267
i : 0 , pid : 574

//子进程部分
i am child process, pid : 575, ppid : 574
i : 0 , pid : 575
i : 1 , pid : 574
i : 1 , pid : 575
i : 2 , pid : 575
i : 2 , pid : 574
i : 3 , pid : 574
i : 3 , pid : 575
i : 4 , pid : 575
i : 4 , pid : 574

从子进程的输出情况看出,父进程和子进程是交替执行的。

ps aux 查找父进程的 ppid: 27267,发现是一个 bash 进程(如下图):
在这里插入图片描述

父子进程用户区数据读写

父子进程虚拟地址空间:
刚才在fork函数中也看到,创建子进程会复制一个新的地址空间:
在这里插入图片描述
Description:
The child process and the parent process run in separate memory spaces. At the time of fork() both memory spaces have the same content. Memory writes, file mappings (mmap(2)), and unmappings (munmap(2)) performed by one of the processes do not affect the other.
子进程和父进程在不同的内存空间中运行。 在执行 fork() 时,两个内存空间的内容相同。 其中一个进程执行的内存写入、文件映射(mmap(2))和解除映射(munmap(2))操作不会影响另一个进程

在上面代码中初始化一个num=10,分别在子进程和父进程中操作num,两者的操作互不影响,效果如下:
在这里插入图片描述
注意:
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术

在这里插入图片描述

面试题: fork()函数会复制整个进程的地址空间吗?

答:内核并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间,只有在需要写入的时候才会复制地址空间

在这里插入图片描述
fork函数返回值是两个位于父子进程的栈空间的局部变量。

总结:读时共享,写时拷贝!

GDB多进程调试


在这里插入图片描述

(gdb) show follow-fork-mode
Debugger response to a program call of fork or vfork is "parent".

(gdb) show detach-on-fork
Whether gdb will detach the child of a fork is on.

如果设置 detach-on-fork 为 off,则子进程会挂起。

查看调试的进程:
‘ * ’ 表示当前调试的进程。

在这里插入图片描述

切换当前的进程:

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

使进程脱离GDB调试:

在这里插入图片描述

会把detach inferiors的进程继续执行完。

5、exec函数族

exec 函数族: 一系列功能相同或相似的函数,类似C++函数重载。

exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就
在调用进程内部执行一个可执行文件

exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,
只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。
看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。
比如,内核区调用了exec函数,当前用户区的内容会被a.out的用户区内容替换掉:
在这里插入图片描述
常用exec函数:

int execl(const char *path, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
//linux系统函数man 2 execve,其余的函数都是在这个函数上的封装 man 2 execl
int execve(const char *filename, char *const argv[], char *const envp[]); 

execl

int execl(const char *path, const char *arg, …);

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

            - arg:是执行可执行文件所需要的参数列表
                第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
                从第二个参数开始往后,就是程序执行所需要的的 参数列表。
                参数最后需要以NULL结束(哨兵)

        - 返回值:
            只有当调用失败,才会有返回值,返回-1,并且设置errno
            如果调用成功,没有返回值 (内容替换成功)。
#include <unistd.h>
#include <stdio.h>

int main() {

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

    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n",getpid());
        sleep(1);
    }else if(pid == 0) {
        // 子进程
        execl("hello","hello",NULL);
        // execl("/bin/ps", "ps", "aux", NULL);//执行shell命令,ps aux 查看当前进程信息
        //execl("ps", "ps", "aux", NULL);//execl: No such file or directory
        //execl("hello", "ps", "aux", NULL); //hello, world
        perror("execl");
        printf("i am child process, pid : %d\n", getpid());
    }

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

    return 0;
}

输出:

i am parent process, pid : 8873
hello, world
i = 0, pid = 8873
i = 1, pid = 8873
i = 2, pid = 8873

可以看到执行子进程时,execl用hello把它替换了,子进程后续内容不再执行。

execlp

int execlp(const char *file, const char *arg, … );

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

            - arg:是执行可执行文件所需要的参数列表
                第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)

        - 返回值:
            只有当调用失败,才会有返回值,返回-1,并且设置errno
            如果调用成功,没有返回值。


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

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

int main() {

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

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

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

    return 0;
}

输出:

i am parent process, pid : 10233
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   9868   656 ?        Ssl  08:57   0:00 /init
root       108  0.0  0.0   9968   324 tty2     Ss   08:57   0:00 /init
petri      109  0.0  0.0  10656   680 tty2     S    08:57   0:00 sh -c "$VSCODE_WSL_EXT_LOCATION/scripts/wslServer.sh" e170252f762678dec6ca2cc69aba1570769a5d39 stable code-server .vscode-server --host=127.0.0.1 --port=0 --connection-token=3276537263
petri      110  0.0  0.0  10656   728 tty2     S    08:57   0:00 sh /mnt/c/Users/17335/.vscode/extensions/ms-vscode-remote.remote-wsl-0.88.0/scripts/wslServer.sh e170252f762678dec6ca2cc69aba1570769a5d39 stable code-server .vscode-server --host=127.0
petri      115  0.0  0.0  10656   720 tty2     S    08:57   0:00 sh /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/bin/code-server --host=127.0.0.1 --port=0 --connection-token=3276537263-925675798-3382388098-1089837822 --use
petri      119  0.1  0.3 995220 111568 tty2    Sl   08:57   0:07 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/server-main.js --host=127.0.0.1
root       202  0.0  0.0   9880   324 tty3     Ss   08:57   0:00 /init
petri      203  0.0  0.1 614816 39208 tty3     Sl   08:57   0:00 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node -e const net = require('net'); process.stdin.pause(); const client = net.createConnection({ host: '127.0.0.
root       210  0.0  0.0   9880   324 tty4     Ss   08:57   0:00 /init
petri      211  0.0  0.1 609696 34860 tty4     Sl   08:57   0:00 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node -e const net = require('net'); process.stdin.pause(); const client = net.createConnection({ host: '127.0.0.
petri      218  0.5  0.4 1035192 146836 tty2   Sl   08:57   0:29 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node --dns-result-order=ipv4first /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/bo
petri      229  0.0  0.1 857652 37300 tty2     Sl   08:57   0:01 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/bootstrap-fork --type=fileWatch
petri      244  0.0  0.1 658344 44016 tty2     Rl   08:57   0:00 /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/node /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/bootstrap-fork --type=ptyHost -
petri      260  0.0  0.1 133412 55900 tty2     Sl   08:57   0:02 /home/petri/.vscode-server/extensions/ms-vscode.cpptools-1.19.9-linux-x64/bin/cpptools
petri      285  0.0  0.0  17140  2724 pts/0    Ss   08:57   0:00 /bin/bash --init-file /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh
petri    10155  0.3  0.0 4280760 8244 tty2     Sl   10:31   0:00 /home/petri/.vscode-server/extensions/ms-vscode.cpptools-1.19.9-linux-x64/bin/cpptools-srv 260 {AB84DDF0-E8B0-49F3-B906-82F973B6A78D}
petri    10224  0.0  0.0  10656   680 tty2     S    10:31   0:00 /bin/sh -c "/home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/vs/base/node/cpuUsage.sh" 285
petri    10225  0.0  0.0  16664  1748 tty2     S    10:31   0:00 /bin/bash /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/vs/base/node/cpuUsage.sh 285
petri    10228  0.0  0.0  15276   816 tty2     S    10:31   0:00 sleep 1
petri    10233  0.0  0.0  10536   576 pts/0    S    10:31   0:00 ./execlp
petri    10234  0.0  0.0  18660  1896 pts/0    R    10:31   0:00 ps aux
i = 0, pid = 10233
i = 1, pid = 10233
i = 2, pid = 10233

总结一下exec后缀的作用:

int execl(const char *path, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
//linux系统函数man 2 execve,其余的函数都是在这个函数上的封装 man 2 execl
int execve(const char *filename, char *const argv[], char *const envp[]); 

在这里插入图片描述

6、进程控制

进程退出

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

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

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

exit和_exit的区别:
在这里插入图片描述

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

int main() {

    printf("hello\n"); // 带\n printf可以自动刷新I/O缓冲区,把hello打印出来,它是标准C库的函数
    printf("world");

    // exit(0);
    _exit(0);
    
    return 0;
}

_exit(0)只输出 hello,因为\n会把hello放到缓冲区里并刷新到控制台上,但是 world 因为_exit(0)不自带刷新缓冲区的功能,所以就不显示了。如果两个printf都不带\n的话,就什么也不会显示了。
所以平时用 exit 比较多一些。

孤儿进程

父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)

每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init (pid=1),而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

孤儿进程并没有什么危害。

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

int main() {

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

    // 判断是父进程还是子进程
    if(pid > 0) {

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

    } else if(pid == 0) {
        sleep(20);
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
       
    }

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

    return 0;
}
输出:
i am parent process, pid : 15056, ppid : 285
petri@XX:~/lesson02/03$ i am child process, pid : 15057, ppid : 1

注意看,子进程打印到下一个终端上了,而且 ppid (父亲进程)是 1,即 init 的 pid。

僵尸进程

每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区PCB(进程控制块) 没有办法自己释放掉,需要父进程去释放。

进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。

操作系统的进程号是有限的。

僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait()waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

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

int main() {

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

    // 判断是父进程还是子进程
    if(pid > 0) {
        while(1) {
            printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
            sleep(1);
        }

    } else if(pid == 0) {
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
       
    }

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

    return 0;
}

输出:

i am parent process, pid : 21018, ppid : 285
i am child process, pid : 21019, ppid : 21018
i : 0 , pid : 21019
i : 1 , pid : 21019
i : 2 , pid : 21019
i am parent process, pid : 21018, ppid : 285
i am parent process, pid : 21018, ppid : 285
i am parent process, pid : 21018, ppid : 285
i am parent process, pid : 21018, ppid : 285
i am parent process, pid : 21018, ppid : 285

因为父进程一直在循环,并不会释放子进程的PCB,导致子进程变为僵尸进程

ps aux

petri    21018  0.0  0.0  10536   576 pts/0    S    12:17   0:00 ./zombie
petri    21019  0.0  0.0      0     0 pts/0    Z    12:17   0:00 [zombie] <defunct>
petri    21044  0.0  0.0  17140  2640 pts/1    Ss   12:17   0:00 /bin/bash --init-file /home/petri/.vscode-server/bin/e170252f762678dec6ca2cc69aba1570769a5d39/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bas
petri    21199  0.0  0.0  18660  1900 pts/1    R    12:17   0:00 ps aux

pid = 21019 的为僵尸进程,此时 kill -9 21019 并不会杀死该僵尸进程,除非kill -9 父进程 21018,子进程变为孤儿进程,然后 被 pid = 1 的 init 进程 做善后处理。
因此,需要 wait() waitpid() 这两个函数。

进程回收

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。
但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)

父进程可以通过调用 waitwaitpid 得到它的退出状态同时彻底清除掉这个进程。

wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,
waitpid() 还可以指定等待哪个子进程结束。

注意:一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环

wait()

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
    功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。
    参数:int *wstatus
        进程退出时的状态信息,传入的是一个int类型的地址,传出参数。这个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;

    // 创建5个子进程
    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {//不可能只产生5个子进程。为了让子进程不再产生进程,加入一个pid == 0来判断,让子进程终止for循环
            break;
        }
    }

    if(pid > 0) {
        // 父进程
        while(1) {
            printf("parent, pid = %d\n", getpid());
            int st;
            int ret = wait(NULL);
            
            // 没有子进程后 wait 函数返回 -1,结束循环
            int ret = wait(&st);
            if(ret == -1) {
                 break;
            }
            
            printf("child die, pid = %d\n", ret);
            sleep(1);
        }

    } else if (pid == 0){
        // 子进程
         while(1) {
            printf("child, pid = %d\n", getpid());    
            sleep(1);       
         }

        exit(0);
    }

    return 0; // exit(0)
}

在这里插入图片描述

父进程只打印了一次,因为wait函数使其阻塞,此时 kill -9 27501 这个子进程:

在这里插入图片描述
父进程被唤醒(相当于继续往下执行),继续执行循环,然后又碰到wait函数,进入阻塞状态。

在这里插入图片描述
修改下父进程代码:

    if(pid > 0) {
        // 父进程
        while(1) {
            printf("parent, pid = %d\n", getpid());
            // int ret = wait(NULL);
            int st;
            int ret = wait(&st);
            if(ret == -1) {
                break;
            }
            if(WIFEXITED(st)) {
                // 是不是正常退出
                printf("退出的状态码:%d\n", WEXITSTATUS(st));
            }
            if(WIFSIGNALED(st)) {
                // 是不是异常终止
                printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
            }
            printf("child die, pid = %d\n", ret);
            sleep(1);
        }

新建一个bash,输入:

kill -9 5647
child, pid = 5648
child, pid = 5647
被哪个信号干掉了:9
child die, pid = 5647
child, pid = 5649
child, pid = 5646

waitpid()

#include <sys/types.h>
#include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *wstatus, int options);
        功能:回收指定进程号的子进程,可以设置是否阻塞。
        参数:
            - pid:
                pid > 0 : 某个子进程的pid
                pid = 0 : 回收当前 进程组 的所有子进程    (ps ajx 可以查看各个进程所属的组)
                pid = -1 : 回收所有的子进程,相当于 wait()  (最常用)
                pid < -1 : 某个进程组的组id的绝对值(负值?),回收指定进程组中的子进程 (比如 -765- options:设置阻塞或者非阻塞
                0 : 阻塞
                WNOHANG : 非阻塞 (宏值,如果没有子进程结束(还有子进程),立刻返回)
            - 返回值:
                > 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;

    // 创建5个子进程
    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            break;
        }
    }

    if(pid > 0) {
        // 父进程
        while(1) {
            printf("parent, pid = %d\n", getpid());
            sleep(1);

            int st;
            // int ret = waitpid(-1, &st, 0);
            int ret = waitpid(-1, &st, WNOHANG);

            if(ret == -1) {
                break;
            } else if(ret == 0) {
                // 说明还有子进程存在
                continue;
            } else if(ret > 0) {

                if(WIFEXITED(st)) {
                    // 是不是正常退出
                    printf("退出的状态码:%d\n", WEXITSTATUS(st));
                }
                if(WIFSIGNALED(st)) {
                    // 是不是异常终止
                    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
                }

                printf("child die, pid = %d\n", ret);
            }
           
        }

    } else if (pid == 0){
        // 子进程
         while(1) {
            printf("child, pid = %d\n",getpid());
            sleep(1);
         }
        exit(0);
    }

    return 0; 
}
child, pid = 16913
child, pid = 16914
child, pid = 16915
child, pid = 16916
parent, pid = 16912
child, pid = 16917
child, pid = 16913
child, pid = 16914
child, pid = 16915
child, pid = 16916
parent, pid = 16912
child, pid = 16917
child, pid = 16913

设置的非阻塞 WNOHANG,此时如果不进行任何操作,ret == 0,所以parent会一直循环输出。

7、进程间通信

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

进程间通信的目的:

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

在这里插入图片描述

匿名管道(管道)

管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。
在这里插入图片描述
在这里插入图片描述

管道的特点

  • 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。

  • 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。

  • 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。

  • 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。读写顺序一致。

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

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

  • 匿名管道只能具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

在这里插入图片描述

管道读写的特点(四种情况):

在这里插入图片描述

1、所有的指向管道写端的文件描述符都关闭了(管道写端的引用计数为0),进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
在这里插入图片描述

2、如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后再次read会阻塞,直到管道中有数据可以读了。
在这里插入图片描述

3、如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数大于0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE,通常会导致异常终止(满了)。
在这里插入图片描述
4、如果有指向管道读端的文件描述符没有关闭(管道读端引用计数大于0),而持有管道读段的进程也没有从管道中读数据,这时有进程向进程管道中写数据,那么管道写满时,再次调用write会阻塞,直到管道中有空位置才能再次写入数据并返回。
在这里插入图片描述

总结:
1、读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
1)写端被全部关闭,read返回0(相当于读到文件的末尾)
2)写端没有完全关闭,read阻塞等待

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

操作的管道读写段是文件描述符

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

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

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

情况一:写端都关闭,然后读取数据

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

int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());

        // 关闭写端
        close(pipefd[1]);
        
        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            printf("%d\n", len);
            sleep(1);
        }

    } else if(pid == 0){
        // 子进程
        printf("i am child process, pid : %d\n", getpid());
        // 关闭读端
        // close(pipefd[0]);
        // 关闭写端
        close(pipefd[1]);
        char buf[1024] = {0};       
    }
    return 0;
}

输出:

i am parent process, pid : 6131
i am child process, pid : 6132
parent recv : , pid : 6131
0
parent recv : , pid : 6131
0
parent recv : , pid : 6131
0
parent recv : , pid : 6131
0

情况二:一个写端关闭,另一个没关闭但不忘里写数据,此时read会阻塞

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

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

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

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());

        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            printf("%d\n", len);
            sleep(1);
          
        }

    } else if(pid == 0){
        // 子进程
        printf("i am child process, pid : %d\n", getpid());

        char buf[1024] = {0};
       // 向管道中写入数据
        char * str = "hello,i am child";
        write(pipefd[1], str, strlen(str));
         
         // 关闭写端
        close(pipefd[1]);
    }
    return 0;
}

输出:在读取了一个数据后父进程再次read会阻塞

i am parent process, pid : 7496
i am child process, pid : 7497
parent recv : hello,i am child, pid : 7496
16

有名管道

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

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

  3. 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一个写入端读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。
    FIFO 的名称也由此而来:先入先出。(队列 queue)

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

    1)FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。
    2)当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
    3)FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。

通过命令创建有名管道:

mkfifo 名字

通过函数创建有名文件:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件I/O 函数都可用于 fifo。
如:close、read、write、unlink 等。
FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 等文件定位操作。

#include <sys/types.h>
#include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
        参数:
            - pathname: 管道名称的路径
            - mode: 文件的权限 和 open 的 mode 是一样的是一个八进制的数
        返回值:
        	成功返回0,失败返回-1,并设置错误号

1、创建一个有名管道:

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

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

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

2、向有名管道写入数据:

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

    // 写数据
    for (int i = 0; i < 10; i++){
        char buf[1024];
        sprintf(buf, "hello, %d \n", i); //写入buf数据:hello, i
        printf("write data: %s\n", buf);
        write(fd, buf, strlen(buf));
        sleep(1);
    }
    //关闭文件描述符
    close(fd); 

3、从有名管道中读取数据

	// 以只读的方式打开管道文件
    int fd = open("fifo1", O_RDONLY);
    if(fd == -1) {
        perror("open");
        exit(0);
    }

    // 读数据
    while(1) {
        char buf[1024] = {0};
        int len = read(fd, buf, sizeof(buf));

        //写端断开连接,管道内数据读完后 read会返回 0
        if(len == 0) {
            printf("写端断开连接了...\n");
            break;
        }
        printf("recv buf : %s\n", buf);
    }
	//关闭文件描述符
    close(fd);

注意事项:
读端关闭,写端一直往里面写会产生一个信号SIGPIPE。上面读和写用两个bash运行,读端停,写端立马结束。
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道。
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道。

例子:使用有名管道完成聊天的功能在这里插入图片描述

chatA.c 的代码如下, 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("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if(ret == -1) {
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2", F_OK);
    if(ret == -1) {
        // 文件不存在
        printf("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if(ret == -1) {
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只写的方式打开管道fifo1
    int fdw = open("fifo1", O_WRONLY);
    if(fdw == -1) {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo1成功,等待写入...\n");
    // 3.以只读的方式打开管道fifo2
    int fdr = open("fifo2", O_RDONLY);
    if(fdr == -1) {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo2成功,等待读取...\n");

    char buf[128];

    // 4.循环的写读数据
    while(1) {
        memset(buf, 0, 128); // 清空buf,每次写都刷新一下相当于
        // 获取标准输入的数据
        fgets(buf, 128, stdin);//stdin: file文件指针
        // 写数据
        ret = write(fdw, buf, strlen(buf));//fifo1
        if(ret == -1) {
            perror("write");
            exit(0);
        }

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

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

    return 0;
}

启动chatA:

petri@XX:~/lesson02/07$ ./A
打开管道fifo1成功,等待写入...

在创建写入管道后阻塞,直到启动chatB把它的读端打开:

petri@XX:~/lesson02/07$ ./B
打开管道fifo1成功,等待读取...
打开管道fifo2成功,等待写入...

这样就可以实现进程间通讯了:
A:

nihao
buf: wohenhao

B:

buf: nihao

wohenhao

不过while循环有些缺点,就是当前情况下B再给A发送消息,必须等到A也发送一个数据后A才能收到B的消息。这种情况可以通过将读写分别放入父子进程(fork)中来实现即使收发消息。

内存映射

内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件
在这里插入图片描述
通过修改进程内存实现修改磁盘中文件的内容。实现映射后,这俩是同步修改的。

#include <sys/mman.h>
 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); //映射
 int munmap(void *addr, size_t length); //接触映射
#include <sys/mman.h>
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
        - 功能:将一个 文件或者设备的数据 映射 到内存中
        - 参数:
            - void *addr: 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参数有冲突。prot权限必须小于等于open的权限,必须要有 读 权限。
                    prot: PROT_READ                open:只读/读写 
                    prot: PROT_READ | PROT_WRITE   open:读写
            - offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不偏移。
        - 返回值:
        	成功:返回创建的内存的首地址
            失败:返回MAP_FAILED,(void *) -1

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

void * :通用指针,是指向任何类型的指针,不能解引用,使用它时需要进行类型转换(类型强制转换)。

// 使用 malloc 分配内存,返回 void *
void *ptr = malloc(n * sizeof(int));

// 将 void * 转换为 int *
int *intPtr = (int *)ptr;

使用内存映射实现进程间通信: 具备有名进程和匿名进程的特性:分有关系和没关系

1.有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区

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

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

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);//prot必须有读权限 
    if(ptr == MAP_FAILED) {
        perror("mmap");
        exit(0);
    } 

    // 3.创建子进程
    pid_t pid = fork();
    if(pid > 0) {
        wait(NULL); // 等待子进程结束再继续执行,子进程写数据,父进程读数据
        // 父进程
        char buf[64];
        strcpy(buf, (char *)ptr);// string copy,  void* 强转为 char*
        printf("read data : %s\n", buf);
       
    }else if(pid == 0){
        // 子进程
        strcpy((char *)ptr, "nihao a, dad!!!");
    }

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

    return 0;
}
read data : nihao a, dad!!!

内存映射注意事项:

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

  1. 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
    可以对ptr进行++操作,但是munmap会错误,需要提前保存++的内存地址
    void * ptr = mmap(…);
    void * ptr2 = ptr;
    ptr++;
    munmap(ptr2, len);

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

  3. 如果文件偏移量为1000会怎样?
    偏移量必须是4k的整数倍, 返回MAP_FAILED (分页大小)

  4. mmap什么情况下会调用失败?

    • 第二个参数: length = 0
    • 第三个参数: prot权限有问题
      • 只指定了写权限
      • prot PROT_READ | PROT_WRITE
        第五个参数fd 通过open函数时 未指定 O_RDONLY | O_WRONLY
  5. 可以open的时候O_CREATE一个新文件来创建映射区吗?

    • 可以, 但是创建文件大小为0的话,肯定不行
    • 可以对新的文件进行扩展
      • lseek()
      • truncate()
  6. mmap后关闭文件描述符,对mmap映射有没有影响?
    int fd = open(“xxx”)
    mmap(, fd, 0);
    close(fd);
    映射区还存在, 创建映射区的fd被关闭, 没有任何影响

  7. 对ptr越界操作会怎样?
    越界操作,操作的是非法内存,-> 段错误

内存映射可以实现有关系和没关系的进程间通信,除此之外,还可以 完成文件复制的功能:

int main(){

    //1、打开原始文件
    int fd = open("english.txt", O_RDWR);
    if (fd == -1){
        perror("open");
        exit(0);
    }

    //2、新文件
    int fd1 = open("cpy.txt", O_RDWR); //打开读写权限
    if (fd1 == -1){
    perror("open");
    exit(0);
    }

    //获得原始文件大小
    int len = lseek(fd, 0, SEEK_END);
    //对新文件进行拓展
    truncate("cpy.txt", len);
    write(fd1, " ", 1);

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

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

    //内存拷贝
    memcpy(ptr1, ptr, len);

    //释放资源,先关闭第二个
    munmap(ptr1, len);
    munmap(ptr, len);

    close(fd1);
    close(fd);

    return 0;
}

匿名映射

内存映射的需要依赖⽂件。⽽建⽴文件建⽴好了只会还要unlink close掉,比较麻烦,所以使用匿名映射实现父子进程间通信

int main() {

    // 1.创建匿名内存映射区
    int len = 4096;
    //flags加上MAP_ANONYMOUS,fd值设置为为-1
    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, "hello, world");
        wait(NULL);
    }else if(pid == 0) {
        // 子进程
        sleep(1);
        printf("%s\n", (char *)ptr);
    }

    // 释放内存映射区
    int ret = munmap(ptr, len);

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

输出:

//等待1秒
hello, world

8、信号

概念

信号是 Linux 进程间通信的最古老的方式之一(管道也是),是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
(比如,正在打游戏然后快递来了,暂停游戏去拿快递,拿完后继续打游戏)

同步和异步:同步是阻塞模式,异步是非阻塞模式。

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

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

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

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

3、信号的特点:

  • 简单
  • 不能携带大量信息(仅仅是一个信号值)
  • 满足某个特定条件才发送(某些特定事情发生)
  • 优先级比较高 (居然能打断我打游戏)

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

 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

前 31 个信号为常规信号(大概了解下就行),其余为实时信号。

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

在这里插入图片描述
4、信号的 5 种默认处理动作:
查看信号的详细信息:man 7 signal

信号的 5 中默认处理动作:

  • Term 终止进程
  • Ign 当前进程忽略掉这个信号
  • Core 终止进程,并生成一个Core文件(对程序错误进行调试)
  • Stop 暂停当前进程
  • Cont 继续执行当前被暂停的进程 (continue)

信号的几种状态:产生、未决(还为被处理)、递达(被处理了)
SIGKILLSIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。

a.cpp

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

执行发生段错误:Segmentation fault (core dumped) 信号:SIGSEGV

命令行输入:ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7823
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 65536
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7823
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

可以看到 core file size 是0,现在执行 ulimit -c 1024,然后再重新执行 a.out ,会生成一个 core 文件,现在 gdb a.out ,可以看到报错的位置:
在这里插入图片描述

信号相关函数

int kill(pid_t pid, int sig);
int raise(int sig);
void abort(void);
unsigned int alarm(unsigned int seconds);
int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);

详细介绍:

kill() raise() abort()

#include <sys/types.h>
#include <signal.h>

    int kill(pid_t pid, int sig);
        - 功能:给 任何 的进程或者进程组 pid, 发送 任何 的信号 sig
        - 参数:
            - pid :
                > 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);   //raise和abort的功能都能用kill实现

    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) {
        // 子进程
        int i = 0;
        for(i = 0; i < 5; i++) {
            printf("%d, child process\n",i);
            sleep(1);
        }

    } else if(pid > 0) {
        // 父进程
        printf("parent process\n");
        sleep(2);
        printf("kill child process now\n");
        kill(pid, SIGINT); // 9号信号 强制杀死
    }

    return 0;
}

输出:

parent process
0, child process
1, child process
kill child process now

多进程会相互抢占CPU的执行权,不太确定谁先执行。

alarm() setitimer()

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

    - SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
        alarm(10);  -> 返回0
        过了1alarm(5);   -> 返回9

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

int main() {

    int seconds = alarm(5);
    printf("seconds = %d\n", seconds);  // 不阻塞,直接执行打印 0

    sleep(2);
    seconds = alarm(2);    // 不阻塞,立马执行下面的语句,由于之前有定时器,所以打印 5-2=3
    printf("seconds = %d\n", seconds);  // 3

    while(1) {//时间到了就终止当前的死循环进程
    }

    return 0;
}
seconds = 0
seconds = 3
Alarm clock

1秒钟电脑能数多少个数?

实际的时间 = 内核时间 + 用户时间 + 消耗的时间
进行文件IO操作的时候比较浪费时间

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

    alarm(1); //定时1秒

    int i = 0;
    while(1) {
        printf("%i\n", i++);
    }
    //./alarm1 >> a.txt  写入文件,更快
    return 0;
}

alarm没法实现周期性的定时任务:于是有了 setitimer()

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

    // 设置间隔的时间,每个2秒定时一次
    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("定时器开始了...\n");

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

    getchar();

    return 0;
}
定时器开始了...  (等了5秒)
Alarm clock

信号集

许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集数据结构来表示,其系统数据类型为 sigset_t (64位的整数数组)。(信号集就是一串整数,每个位置对应不同的信号命令)

PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为 “未决信号集” 。这两个信号集都是内核使用位图机制(二进制位,之前学的flag权限 umask之类)来实现的。

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

信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作

信号集相关操作函数

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

    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 : 失败

关于sigset_t 数据类型,从源码可以看出就是一个包含64位的整数数组的结构体:

typedef struct
{
  unsigned long int __val[(1024 / (8 * sizeof (unsigned long int)))];
} __sigset_t;
#include <signal.h>
#include <stdio.h>

int main() {

    // 创建一个信号集
    sigset_t set;

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

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

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

    // 判断SIGINT是否在信号集中
    ret = sigismember(&set, SIGINT); //SIGINT 阻塞
    if(ret == 0) {
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGINT 阻塞\n");
    }

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

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);
    
    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT); //SIGQUIT 不阻塞
    if(ret == 0) {
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGQUIT 阻塞\n");
    }

    return 0;
}
sigprocmask() 和 sigpending()

如果是内核中的信号集,则需要用 sigprocmask()sigpending() 这两个函数:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    - 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
    - 参数:
        - how : 如何对内核阻塞信号集进行处理
            SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
                假设内核中默认的阻塞信号集是 mask, mask | set  异或(把原来为0的设置为1,用异或)
            SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
                mask &= ~set 按位与 set取反 (把原来为1的设置为0,用按位与)

            比如:
            mask: 1 0 0 1 0  set: 0 0 1 1 0
            mask | set: 1 0 1 1 0  把阻塞信号加入内核

            ~set: 1 1 0 0 1
            mask & ~set: 1 0 0 1 0  对内核中数据解除阻塞


            SIG_SETMASK:覆盖内核中原来的值
        
        - set :已经初始化好的用户自定义的信号集
        - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
    - 返回值:
        成功:0
        失败:-1
            设置错误号:EFAULT、EINVAL

int sigpending(sigset_t *set);
    - 功能:获取内核中的未决信号集
    - 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
    - 返回值:
        成功:0
        失败:-1
#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);
    // 0 1 1 0 0

    // 修改内核中的阻塞信号集  (用自定义的去修改内核中的)
    sigprocmask(SIG_BLOCK, &set, NULL);

    int num = 0;

    while(1) {
        num++;
        // 获取当前的 未决信号集 的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset); //获取内核中的未决信号集 返回修改后的pendingset(注意是内核中的,这样就能和上面的sigprocmask关联起来)

        // 遍历前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 == 10) {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }

    }
    return 0;
}

打印未决信号集:

0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000

Ctrl + C 会产生2号信号,未决信号集的2号位置就变为了1(未决状态),由于 上述代码将阻塞信号集的2号位置设置为阻塞状态,所以一直处于未决状态直到接触阻塞 sigprocmask(SIG_UNBLOCK, &set, NULL) :
在这里插入图片描述
上图和代码的解释:
1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)

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

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

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

补充一点:
前台进程和后台进程:
切换至后台进程:./sigprocmask &
切换至前台进程:fg

信号捕捉函数(signal和sigaction)

signal

sighandler_t signal(int signum, sighandler_t handler);

typedef void (*sighandler_t)(int); //函数指针,函数的地址

#include <signal.h>

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

       - 返回值:
            成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败,返回SIG_ERR,设置错误号
            
    SIGKILL SIGSTOP不能被捕捉,不能被忽略。(防止病毒程序自定义捕捉行为)
void myalarm(int num) {
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

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

    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    // signal(SIGALRM, SIG_DFL);
    // void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
    signal(SIGALRM, myalarm); // 信号捕捉,要在设置定时器setitimer之前

    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("定时器开始了...\n");

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

    getchar();

    return 0;
}

setitimer 五秒后会发送SIGALRM信号,然后被signal捕捉并执行myalarm函数中的内容:

捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx

sigaction

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

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

     struct sigaction {
        // 函数指针,指向的函数就是信号捕捉到之后的处理函数
        void     (*sa_handler)(int);
        
        // 不常用
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        
        // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
        sigset_t   sa_mask; //一般要先清空 用sigemptyset(&act.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("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

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

    struct sigaction act;
    act.sa_flags = 0; //使用sa_handler处理
    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("定时器开始了...\n");

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

    // getchar();
    while(1); //作用是让程序不结束。以一直捕捉信号

    return 0;
}
在这里插入代码片定时器开始了...
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx
捕捉到了信号的编号是:14
xxxxxxx

尽量多使用 sigaction,不同操作系统的兼容性比signal更好。
在这里插入图片描述
内核用程序员写的回调函数处理信号。

SIGCHLD信号

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

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("捕捉到的信号 :%d\n", num);
    // 回收子进程PCB的资源
    // while(1) {
    //     wait(NULL); 
    // }
    while(1) {
       int ret = waitpid(-1, NULL, WNOHANG);
       if(ret > 0) {
           printf("child die , pid = %d\n", ret);
       } else if(ret == 0) {
           // 说明还有子进程活着,break 父进程继续执行
           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 < 10; i++){
        pid = fork();
        if (pid == 0){//保证生成10个子进程
            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;
}

可以看到,捕捉到信号SIGCHLD后对子进程PCB资源进行了回收:

child process pid : 29374
child process pid : 29375
child process pid : 29376
child process pid : 29377
child process pid : 29378
child process pid : 29379
child process pid : 29380
child process pid : 29381
child process pid : 29382
捕捉到的信号 :17
child process pid : 29383
child die , pid = 29374
child die , pid = 29375
child die , pid = 29376
child die , pid = 29377
child die , pid = 29378
child die , pid = 29379
child die , pid = 29380
child die , pid = 29381
child die , pid = 29382
child die , pid = 29383
捕捉到的信号 :17
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373
parent process pid : 29373

共享内存

共享内存内存映射的效率高。

共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入
所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。

管道等要求 发送进程 将数据从 用户空间的缓冲区 复制进 内核内存 和 接收进程 将数据从 内核内存 复制进 用户空间的缓冲区 的做法相比,共享内存这种 IPC 技术的速度更快

使用步骤

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

总结:创建共享内存(shmget)——>进程关联共享内存(shmat)——>进程与共享内存分离(shmdt)>删除共享内存(shmctl)

共享内存相关函数

#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,后面操作共享内存都是通过这个值。

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

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

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

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

在这里插入图片描述
共享内存,一个进程写,一个进程读,可以实现进程间通信(IPC):
创建两个bash分别执行write和read:
write.cpp

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

int main() {    

    // 1.创建一个共享内存
    int shmid = shmget(1000, 4096, IPC_CREAT|0777);
    printf("shmid : %d\n", shmid);
    
    // 2.和当前进程进行关联
    void * ptr = shmat(shmid, NULL, 0); //ptr 共享内存的首(起始)地址

    char * str = "helloworld";

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

    printf("按任意键继续\n");
    getchar();

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

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

    return 0;
}

输出:

shmid : 1
按任意键继续

read.cpp:

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

int main() {    

    // 1.获取一个共享内存
    //int shmget(key_t key, size_t size, int shmflg); 成功:返回共享内存的引用的ID
    int shmid = shmget(1000, 0, IPC_CREAT);
    // if (shmid == -1) perror(shmget);
    printf("shmid : %d\n", shmid);
    
    // 2.和当前进程进行关联
    //void * shmat(int shmid, const void *shmaddr, int shmflg); //指针函数,返回值为指针类型的函数
    //成功:返回共享内存的首(起始)地址 shmaddr。  失败(void *) -1
    void * ptr = shmat(shmid, NULL, 0);

    // 3.读数据
    printf("%s\n", (char *)ptr);
    
    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    //int shmdt(const void *shmaddr); 
    shmdt(ptr);

    // 5.删除共享内存
    //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

输出:

shmid : 1
helloworld
按任意键继续

常见问题

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

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

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

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

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

共享内存操作命令

在这里插入图片描述

执行上述 write 和 read 时,输入 ipcs -m 可以看到有两个连接:在这里插入图片描述
此时,关闭read,会执行解除关联,连接数变为1,但是共享内存还不能被删除,处于:标记删除状态,键值为0x00000000。

在这里插入图片描述
M 是键, m 是 shmid。
解除所有关联后,执行 ipcrm -m 3 删除shmid为 3 的共享内存。

9、守护进程

1、在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为 shell 进程的控制终端(Controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由 shell 进程启动的其它进程的控制终端也是这个终端(意思就是一个代码里fork出了子进程,子进程也是共享同一个控制终端)。

查看当前终端的PID:

petri@XX:~/lesson02$ echo $$
25328

查看当前终端:

petri@XX:~/lesson02$ tty
/dev/pts/0

查看终端中的进程信息:

petri@XX:~/lesson02$ ps aux 25328
输出:
petri    25328  0.0  0.0  17140  2636 pts/0    Ss   13:47   0:00 /bin/bash

新建一个bash:输入tty:

/dev/pts/1

2、默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。

3、在控制终端输入一些特殊的控制键可以给前台进程发信号(后台进程不行,它没有控制终端),例如 Ctrl + C 会产生 SIGINT 信号,Ctrl + \ 会产生 SIGQUIT 信号。

4、进程组会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。

会话 > 进程组 > 进程

5、进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程 ID 为该进程组的 ID,新进程会继承其父进程所属的进程组 ID

6、进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。

7、会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。

简单来讲:会话/进程组的首进程ID就是会话/进程组的ID,新进程会继承父进程的会话/进程组的ID。

8、一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。(控制终端里有很多会话,会话里有很多进程组,进程组里有很多进程)

9、在任一时刻,会话中的其中一个进程组会成为终端的前台进程组其他进程组会成为后台进程组只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。

10、当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。

在这里插入图片描述
控制终端会与bash会话自动连接。

ctrl+z(挂起)、ctrl+c(中断)、ctrl+\(退出)的区别

  • ctrl+c强行中断当前程序的执行。
  • ctrl+z将任务中断,但是此任务并没有结束,他仍然在进程中,只是放到后台并维持挂起的状态。如需其在后台继续运行,需用“bg
    进程号”使其继续运行;再用"fg 进程号"可将后台进程前台化。
  • ctrl+\表示退出

比如,输入 wc -l & 执行一个后台进程,或者 sort 一个前台进程并通过 ctrl+z 中断并放入后台,此时 可使用 fg 将其转为前台进程并 ctrl+c强行中断,这样在 ps aux 中就看不到他们啦。

进程组、会话操作函数

◼ pid_t getpgrp(void); 获得当前进程组
◼ pid_t getpgid(pid_t pid); 获得当前进程组或进程组id
◼ int setpgid(pid_t pid, pid_t pgid); 设置进程组id
◼ pid_t getsid(pid_t pid); 获取会话的id
◼ pid_t setsid(void); 设置会话id

守护进程

守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。

守护进程具备下列特征:
 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)。(后台一直运行,控制终端控制不了

Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd, 远程登陆服务器 sshd 等。

守护进程的创建步骤:

◼ 执行一个 fork(),之后父进程退出子进程继续执行

◼ 子进程调用 setsid() 开启一个新会话。(脱离控制终端,而上图创建bash会话时,控制终端会自动连接)
(这里也能解释为什么用子进程调用,因为子进程会话id与父进程相同100,但他自己的进程号101与父进100程不相同,创建出来的新会话id101不会与他本身的会话id100冲突)

◼ 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限

◼ 修改进程的当前工作目录,通常会改为根目录(/)。(守护进程会一直运行到系统关闭,根目录磁盘不会被卸载)

◼ 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。(如果要卸载之前父进程打开着的磁盘,是无法卸载的)

◼ 在关闭了文件描述符0、1、2之后,守护进程通常会打开 /dev/null 并使用 dup2() 使所有这些描述符指向这个设备。(dup2() 重定向到 /dev/null)

◼ 核心业务逻辑。

/*
    写一个守护进程,每隔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); //time_t时间结构体,年月日时分秒...
    struct tm * loc = localtime(&tm); //time_t转为本地时间
    char buf[1024];

    sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
    ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);

    printf("%s\n", buf);

    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/");

    // 5. 关闭文件描述符1、2、3,并重定向文件描述符
    //(注释掉下面代码(不重定向),才能输出到控制台!!不然运行程序控制台是没反应的,但能ps aux看到后台程序)
    // int fd = open("/dev/null", O_RDWR);
    // dup2(fd, STDIN_FILENO); //标准输入
    // dup2(fd, STDOUT_FILENO); //标准输出
    // dup2(fd, STDERR_FILENO); //标准错误

    // 6.业务逻辑:每隔2s获取一下系统时间

    struct itimerval val; //时间结构体 
    //过两秒开始执行
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    //间隔时间2秒
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0; //值为0,表示使用sa_handler
    act.sa_handler = work; //函数指针,指向信号捕捉到之后的处理函数
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);


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

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

    return 0;
}

输出效果就是每隔两秒输出一个时间。

终于弄完这一章了,断断续续花了快一个月,一共5w多字!

  • 15
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值