简 述: 本篇主要讲解如下知识点:

  • 并行和并发的区别
  • 进程控制块 PCB 和进程的五种状态
  • 使用 fork() 创建子进程,创建多个兄弟子进程(不含孙进程)
  • 验证进程之间没有共享全局变量
  • exec() 函数族的使用:execl()execlp()

[TOC]


本文初发于 “偕臧的小站“,同步转载于此。


编程环境:

  💻: uos20 📎 gcc/g++ 8.3 📎 gdb8.0

  💻: MacOS 10.14 📎 gcc/g++ 9.2 📎 gdb8.3


并行和并发:

并发:一个时间段,处理请求的个数

eg:在一个时间段内, 处理的请求个数。 只有一个 cpu 对任务进行处理。 (每个人都吃一口饭,但是所有人都没有吃饱,短时间内让大量的快速吃,然后这样轮循环。)

并行:多个进程同时进行任务分配:

可以看做,有多个 cpu 对多个任务进行同时处理。


PCB 和 进程的五种状态:

进程控制块 PCB:

此部分了解即可: 每一个进程在内核中都有一个进程控制块 PCB 来维护进程相关的信息,其是有一个 400 多行的结构体组成,其中主要的需要了解的部分如下:

  • 进程 id。系统中每一个进程有唯一的一个 id,在 C 语言中,用 pid_t 类型表示。(凡是 xxx_t 一般都是 #define 重新定一个类型, 这里实际就上课非负整数)
  • 进程的状态,有就绪,运行,挂起,停止 等状态。(还有一个初始态
  • 进程切换时候,需要保存和恢复一些的 CPU 寄存器
  • 描述虚拟地址空间的信息(每启动一个进程,就对应一个虚拟地址空间)
  • 描述控制终端的信息
  • 当前的工作目录
  • umask 掩码(执行 umask)
  • 文件描述符表,包含很多执行 file 结构体的指针(一个进程最多打开 1024 个)
  • 和信号相关的信息
  • 用户 uid 和组 gid (用stat xxx 可以查看)
  • 会话(Session)和进程组。(会话就是多个进程组)
  • 进程可以使用的资源上限(Resource Limit)。(使用 ulimit -a 可看)

进程的五种状态:

看着如下的这张图,想起里了我之前初看的计算机操作系统的相关知识的时候,感慨颇多。

  • 状态切换的关键:
    • 获得 cpu
    • cpu 执行

进程控制块,父进程创建子进程的分析 🎃:

父子进程空间,其虚拟地址空间是完全一样的(用户区的所有数据都是一样的,也是会运行的相同的一段代码;系统区也是一样的,唯一不一样的就是系统区的 id 号)。

创建一个子进程,是使用 int fork(void) 函数来创建,在执行完这个 fork 函数之后,将用户区的内容全部拷贝(包括接下来要执行的代码)过去。且该 fork 函数有两个返回值,执行完 fork() 后,父进程会返回子进程的 id(大于 0),子进程会返回一个 0,后面执行相同的代码,依靠这个来区分父子进程。 下面写一个代码片来表明:

对照上面的两个图,可以思考如下问题:

  • fork 之后,函数的返回值?
    • fork() 返回值 > 0,就是父进程;fork() 返回值 == 0,就是子进程
  • 子进程创建成功之后,代码的执行位置?
    • 父进程执行哪里(才创建子进程),子进程就重哪里里开始执行。
  • 父子进程的执行顺序?
    • 不一定,自己去抢占 cpu 资源。执行顺序是程序员无法控制的。(cpu 轮循执行)
  • 如何区分父子进程?
    • 通过 fork() 返回值
  • 如何获取当前进程和父进程的 id?
    • 获取进程 id:获取当前进程的 PID:getpid() ;获取当前进程的父进程的 PID: getppid()

对与上面的问题,写如下代码片段进程测试,main.cpp 里面的代码是:代码下载 main.cpp

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

int main(int argc, char *argv[])
{
    for (int i = 0; i < 4; ++i) {
        printf("++++i = %d\n", i);
    }

    pid_t pid = -1;

    pid = fork();

    if (pid > 0)
        printf("this is a parent process, pid = %d\n", getpid());
    else if (pid == 0)
        printf("this is a child process, pid = %d  ppid = %d\n", getpid(), getppid());

    for (int i = 0; i < 4; ++i) {
       printf("----i = %d\n", i);
    }

    return 0;
}

其运行结果如下:


父进程如何创建多个兄弟子进程(不含孙进程)🎃:

看了上面的小的例子之后,发现这样可以创建子进程;进一步,怎么创建多个子进程,但是不要孙创建进程?有怎么判断子进程的顺序编号?怎么得到最开始父进程?

代码如下: 代码下载 childProcess.cpp

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

int main(int argc, char *argv[])
{
    pid_t pid = -1;

    int i = 0;

    for (i = 0; i < 3; ++i) {
        pid = fork();

        if (pid == 0)  //如果当前进程是子进程,就跳出此循环,不在进行 fork() 创建新的子进程
            break;
    }

    if (i == 0)
        printf("this is a child process (i == 0), pid = %d  ppid = %d\n", getpid(), getppid());

    if (i == 1)
        printf("this is a child process (i == 1), pid = %d  ppid = %d\n", getpid(), getppid());

    if (i == 2)
        printf("this is a child process (i == 2), pid = %d  ppid = %d\n", getpid(), getppid());

    if (i == 3)
        printf("this is a parent process (i == 3), pid = %d  ppid = %d\n", getpid(), getppid());

    return 0;
}

分析代码片:

若是没有上面的 13、14 行代码,那么创建的就不止 3 个子进程;就是如下图片所示的结果; 使用 fork() 创建子进程的时候,是进行虚拟地址控件的拷贝(可在草稿纸上面画图理解,是一个地址块所有的代码和变量值都复制一份),他们的当时 i 是 1 的时候,创建的新的子进程的 i 也是 1,不会是初值 0;同理,当 i 是 2 时候创建新进程,其新进程的 i 值是 2,而不是 1; 但是禁止子进程创建孙进程,那么就可以使用 for() 循环生成多个兄弟子进程,且没有任何的孙进程。

运行结果:

可以自己每运行行代码,标记一下 i 的值,以及此 i 的值时刻复制当前进程的虚拟地址空间,给新的子进程;所以 可以通过编号 i 的数值,来判断兄弟子进程的先后顺序;最后编号最大的那个,就是最初的父进程。


进程之间是否共享全局变量:🎃

  • 读时共享(同一个物理内存的上面区域,内存地址为同一个变量)

  • 写的时候复制

  • 父子进程之间能否使用全局变量通信?

    • 不能。因为两个进程之间内存不共享(读时共享,写时复制)

    代码验证,改写上面的代码,添加一个全局变量 int g_num = 200,进行验证:

    代码下载 sharedGlobalvariable.cpp

      #include <stdio.h>
      #include <unistd.h>
      
      
      int g_num = 200;
      
      int main(int argc, char *argv[])
      {
          pid_t pid = -1;
      
          int i = 0;
      
          for (i = 0; i < 3; ++i) {
              pid = fork();
      
              if (pid == 0)  //如果当前进程是子进程,就跳出此循环,不在进行 fork() 创建新的子进程
                  break;
          }
      
      
          if (i == 0) {
              g_num += 5;
              printf("this is a child process (i == 2), pid = %d  ppid = %d\n", getpid(), getppid());
          }
      
      
          if (i == 1){
              g_num += 5;
              printf("this is a child process (i == 2), pid = %d  ppid = %d\n", getpid(), getppid());
          }
      
          if (i == 2) {
              g_num += 5;
              printf("this is a child process (i == 2), pid = %d  ppid = %d\n", getpid(), getppid());
          }
      
          if (i == 3) {
              g_num += 100;
              printf("this is a parent process (i == 3), pid = %d  ppid = %d\n", getpid(), getppid());
          }
      
          printf("g_nmu = %d\n", g_num);
      
          return 0;
    }

    运行截图:

    草稿图分析:


显示当前进程的状态 ps:

  • ps aus 显示不依赖于终端的进程(终端是用来和用户进行交互的)
  • ps aux | grep 查找的进程名 使用管道查找指定的进程名称
  • ps ajx 显示更多的进程信息; PID、PPID、PGID、SID(进程,父进程,组进程,会话)(会话就是多个组进程)
  • ps ajx | grep 查找的进程名 若是没有找到,也会有一行显示信息,显示的是 grep 进程相关的信息

删除执行中的程序或工作 kill:

Linux kill命令用于删除执行中的程序或工作。

kill可将指定的信息送至程序。预设的信息为SIGTERM(15),可将指定程序终止。若仍无法终止该程序,可使用SIGKILL(9)信息尝试强制删除程序。程序或工作的编号可利用ps指令或jobs指令查看。

  • kill -9 将被杀死的进程 -9 为 杀死信号
  • kill -l 显示所有,1-31 通常使用;32 和 33 没有; 34-51 号通常为系统预留

使用 kill 命令之后,有一点困惑,使用 kill -l 命令之后;

  • 在 Ubuntu 18.4 显示如下, 和 MacOS 显示有区别, 有清楚的,可以在下面留言帮我解答一下也可以哦。

  • 只有第9种信号(SIGKILL)才可以无条件终止进程,其他信号进程都有权利忽略。 下面是常用的信号:

    HUP     1    终端断线
    INT     2    中断(同 Ctrl + C)
    QUIT    3    退出(同 Ctrl + \)
    KILL    9    强制终止
    TERM   15    终止
    CONT   18    继续(与STOP相反, fg/bg命令)
    STOP   19    暂停(同 Ctrl + Z)

exec 函数族 🎃:

作用: 让父子进程执行不相干的操作;能够替换进程空间中源代码的 .txt 段;当前程序中调用另外一个应用程序 (调用 exec 之前,需要 fork );

exec() 函数族, 也不要判断返回值:若是函数执行成功,不返回(.text 的代码立即被全部替换,后面的代码也不会执行);若是执行失败,可以调用 perror(“xx 提示:”) 来打印错误信息,退出当前进程;


执行指定目录下的程序 execl():

int execl(const char *path, const char *arg, ... /*, (char *)0 */);
  • path: 需要执行的程序的绝对路径(推荐绝对路经)

  • 变参 arg: 要执行的程序的需要的参数

    • 第一个 arg: 占位(内容随便写什么,但不可为空)
    • 后边的 arg: 命令的参数
  • 参数写完之后: NULL (作用是哨兵,表示该命令的参数已经输入完毕)

  • 一般是执行自己写的程序(也可以执行系统自带的命令)

  • 写一个例子验证一下:代码下载

    • 代码如下:
    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
        for (int i = 0; i < 3; i++)
            printf("-----i = %d\n", i);
    
        pid_t pid = fork();  //创建子进程
    
        if (pid == 0)
            execl("/bin/ls", "占位参数", "-al", NULL);  //ls 程序使用子进程的地址空间
        
        for (int i = 0; i < 3; i++)         //这段打印只会(父进程)被执行一遍。
            printf("+++++i = %d\n", i);   
    
        return 0;
    }
    • 过程分析:

      第一段打印”—–i =” 内容,是只会被父进程执行一遍的,子进程不会执行该段代码(分析见上);结束时候打印 “+++++i =” 内容时候,是只会执行一遍的,还是被父进程执行的,而子进程不执行 的原因是,当第 12 行的代码execl(“ls”) 执行结束之后,其子进程的虚拟地址空间的以用户区域的 .text 代码段的二进制代码被替换为 /bin/ls 这个程序的代码;其后面面也都是运行 ls 程序的代码内容。

    • 运行结果:


执行 PATH 目录下的程序 execlp():

int execlp(const char *file, const char *arg, ... /*, (char *)0 */);
  • file: 需要执行的程序的名字
  • 变参 arg: 要执行的程序的需要的参数
    • 第一个 arg: 占位(内容随便写什么,但不可为空)
    • 后边的 arg: 命令的参数
  • 参数写完之后: NULL (作用是哨兵,表示该命令的参数已经输入完毕)
  • 一般是执行系统自带的命令(也就是 /bin 下的程序)
    • execlp 执行自定义程序的程序: file 参数绝对路径

下载/系列地址:

https://github.com/xmuli/linuxExample

欢迎 star 和 fork 这个系列的 linux 学习,附学习进阶的路线图。