进程 Process

1. 什么是进程

程序原码:存储在硬盘中的代码,可视化文件

可执行程序:原码通过编译操作,得到的二进制可执行文件。

进程: 当前可执行程序在交给 CPU 执行对应的一个【执行实例】,包括当前程序执行所需的相关资源,例如 CPU 占用,计数器,寄存器,变量,可执行的函数和【文件描述符】......

程序是不可变的,但是进程是可变的

  • 进程在运行过程中,会对当前程序执行所需的相关资源,相关逻辑根据执行的情况下,执行不同的模块,申请不同的执行方式。。。
  • 程序是一堆指令的集合,而进程是变化的【执行实例】,进程存在多种状态,
  • 进程执行所需的资源,包括但不限于,CPU,内存,硬盘,IO,网络,外设

2. 进程的状态和转换

进程状态有

  • 就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间
  • 执行态:当前进程正在 CPU 中执行
  • 等待态:进程因为一些原因不具备执行条件,无法获取到 CPU 的处理时间,无法执行。

CPU 时间片概念

02-CPU时间片概念.png

3. 进程控制块

操作系统 (OS) 根据进程控制块 (PCB) 对当前系统中,执行的进程进行管理和控制。当进程创建时,会在内存中申请必要的内存空间,用于管理当前进程相关资源

主要包括

  • 调度数据
    • 包括管理进程状态,标记,优先级和调度策略
  • 时间数据
    • 创建进程的时间,运行状态时间**【用户态执行时间,内核态执行时间】**
  • 文件系统管理
    • 核心【文件描述符】,掩码,内存数据,进程的上下文相关内容,进程号(PID)

4. 进程控制【重点】

4.1 进程号,父进程号和进程组号

每一个进程都有一个唯一的进程号,操作系统用于管理进程的方式。进程号的范围是 0 ~ 32767。

系统中 0 号和 1 号进程都是由系统占用,0 号进程是交换进程(Swapper), 1 号进程是系统的 init 初始化进程。Linux 中任何一个进程号都不可能是负数。

进程相关的编号有三个

  • 进程号 PID Process ID
    • 当前进程的非负数进程号
  • 父进程号 PPID Parent Process ID
    • 在 Linux 中,任何一个进程都需要其他进程进行创建/申请,认为创建/申请当前进程的 Process 是一个父进程
  • 组进程号 PGID Process Group ID
    • 进程组是多个进程的集合,进程组中的进程可以获取同一个终端中发送的信号,信息....

相关函数

涉及到的头文件

#include <unistd.h>
#include <sys/types.h>
pid_t getpid(void);

函数功能: 得到当前进程对应的 PID 号,当前数据对于整个操作系统唯一

返回值类型是一个 pid_t ,可以认为是一个整数类型,对应的真实数据类型为 short 类型。

pid_t getppid(void);

函数功能: 获取当前进程的父进程 ID 数据

返回值类型是一个 pid_t ,可以认为是一个整数类型,对应的真实数据类型为 short 类型。

pid_t getpgid(pid_t pid);

函数功能: 获取指定进程 ID 对应的进程组 ID 数据,如果给予实际参数为 0,表示获取当前进程对应的进程组 ID

参数: 要求获取进程组 ID 的对应进程 PID

返回值: 当前进程对应的进程组 ID 号,如果获取失败,返回 -1

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

int main(int argc, char const *argv[])
{
    pid_t pid = getpid();
    printf("pid : %d\n", pid);

    pid_t ppid = getppid();
    printf("ppid : %d\n", ppid);

    pid_t pgid = getpgid(pid);
    printf("pgid : %d\n", pgid);

    int num = 0;
    scanf("%d", &num);

    return 0;
}

4.2 进程创建函数 fork 【重点】

函数文档

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

pid_t fork(void);
  • 功能

    • fork 函数用于从一个已经运行的进程,创建一个新的进程,新进程称之为【子进程】,已经存在运行的进程可以认为是【父进程】
  • 返回值:

    • 如果在子进程中,返回值为 0,对应当前子进程本身
    • 如果在父进程中,返回值是对应当前子进程的 PID
    • 如果创建失败,返回 -1,表示当前进程没有创建对应子进程
  • 内存空间/地址空间

    • fork 函数会将父进程相关资源完全拷贝一份提供给子进程,包括进程上下文,进程堆栈,【打开的文件描述符】,信号控制设定/规则,进程优先级设计,进程组号
    • fork 操作仅在子进程中,确定了子进程自己【PID】和计时器【Timer】。fork 操作对于进程操作消耗较大。
  • 读时共享,写时拷贝

    准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。写时拷贝是一种可以推迟甚

至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址

空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。

也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。

注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的

文件表,引用计数增加,共享文件偏移指针。

读时共享,写时拷贝;可以降低内存消耗、减少复制所使用的时间!

解释:

(1)读时共享:这个容易理解,即不涉及写操作时,子进程并不会复制父进程的地址空间,而是和父进程共享一块地址空间;

(2)写时拷贝:拿上述代码案例来说,父进程执行部分和子进程执行部分都会对变量num进行操作,但是父进程和子进程对变量num的操作是独立的、互不相干的,此时便需要子进程复制父进程的地址空间,然后父、子进程在各自空间中对变量num进行操作;

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

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

    // 利用 fork 申请创建子进程
    pid = fork();

    if (-1 == pid)
    {
        perror("fork failed");
    }

    /*
    fork
        1. 如果执行的是子进程,pid = 0
        2. 如果执行的是父进程 pid 是当前父进程创建的子进程唯一 PID 数据
    */

    if (0 == pid) 
    {
        // 如果 pid 为 0 表示当前执行的是子进程内容
        while (1)
        {   
            printf("This is Son Process Running!, Flag : %d, PID : %d, PPID : %d, PGID : %d\n"
                , pid                  // fork 函数返回值
                , getpid()             // 当前进程 PID
                , getppid()            // 当前进程的 PPID 父进程 ID
                , getpgid(getpid()));  // 当前进程的 PGID 进程组 ID
            sleep(1);
        }
    }
    else
    {
        // 如果不唯一,表示当前执行的是父进程内容
        while (1)
        {
            printf("This is Parent Process Running!, Flag : %d, PID : %d, PPID : %d, PGID : %d\n"
                , pid                  // fork 函数返回值
                , getpid()             // 当前进程 PID
                , getppid()            // 当前进程的 PPID 父进程 ID
                , getpgid(getpid()));  // 当前进程的 PGID 进程组 ID
            sleep(1);
        }
    }

    return 0;
}

自行测试,在 main 函数定义变量,子进程执行 -- ,父进制执行 ++ 操作,有什么情况

4.3 进程挂起函数 sleep

函数文档

  • 在哪一个进程中执行,哪一个进程进入挂起状态
#include <unistd.h>

unsigned int sleep(unsigned int seconds);
  • 参数解释:
    • 要求提供的参数是一个无符号整数,确定当前进程挂起的秒数
  • 返回值:
    • 如果请求的睡眠时间已到,则返回 0;如果该调用被信号处理程序中断,则返回剩余的睡眠时间(以秒为单位)。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

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

    pid = fork();

    if (-1 == pid)
    {
        perror("fork");
    }

    if (0 == pid)
    {
        int second = 5;
        while (second > 0)
        {
            printf("Son Process running! %d second left!\n", second);
            sleep(1);
            second -= 1;
        }

        _exit(1);
    }
    else
    {
        int second = 5;
        while (second > 0)
        {
            printf("Parent Process running! %d second left!\n", second);
            sleep(1);
            second -= 1;
        }

        _exit(1);
    }

    return 0;
}

4.4 进程等待

4.4.1 wait 函数

进程等待存在两个函数,分别是 wait 和 waitpid 函数

函数文档

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

pid_t wait(int *wstatus);
  • 功能:
    • 函数会使调用进程(通常是父进程)阻塞,直到它的一个子进程终止。当子进程终止时,wait 函数会返回该子进程的 PID,并通过 wstatus 参数返回子进程的退出状态信息。同时会将子进程使用的内存空间进行释放
  • 参数:
    • 参数是一个 int 类型变量的空间首地址,当前变量的作用是存储进程关闭的状态信息。
  • 返回值
    • 若成功,返回终止子进程的进程 ID(PID)。
    • **若出错,返回 -1,并设置 **errno 来指示错误类型。
      • ECHILD:调用进程没有子进程。
      • EINTR:在等待子进程时被信号中断。
  • 补充
    • wait 函数通常在开发中会配合两个系统提供的带参数宏,进行进程退出情况判断和退出信息获取
    • WIFEXITED(status) 若子进程正常终止(通过 exit 或 return),则返回非零值。
    • WEXITSTATUS(wstatus):若 WIFEXITED 为真,则返回子进程的退出状态码(即 exit 或 return 中指定的值)。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define FORK_ERROR_FLAG -1
#define SON_PROCESS 0

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

    pid = fork();

    if (FORK_ERROR_FLAG == pid)
    {
        perror("Fork Failed!");
    }

    /*
    子进程运行 5 秒
    父进程执行时,直接 wait,当前子进程退出时,获取子进程退出状态信息。
    利用 C 语言提供的两个带参宏得到当前的进程退出情况和退出状态
        WIFEXITED(int status)
        WEXITSTATUS(int status)
    */
    if (SON_PROCESS == pid)
    {
        int second = 5;

        while (second > 0)
        {
            printf("Son process running! %d second left!\n", second);
            second -= 1;
            sleep(1);
        }

        // 利用 System Calls 关闭当前线程,退出状态码为 1
        _exit(2);

        #if 0
        // C 库函数退出
        exit(1);
        #endif
        
    }
    else
    {
        int second = 5;
        int status = 0;

        printf("Parent process waiting.......\n");
        // wait 函数执行,将当前子进程退出的状态信息,存储到 status 中.
        wait(&status);

        /*
        WIFEXITED(status) 若子进程正常终止(通过 exit 或 return),则返回非零值。
        */
        if (WIFEXITED(status))
        {
            // 子进程正常退出
            /*
            WEXITSTATUS(wstatus):
                若 WIFEXITED 为真,则返回子进程的退出状态码(即 exit 或 return 
                中指定的值)。
            */
            int status_code = WEXITSTATUS(status);

            printf("Son process exit successful, status : %d\n", status_code);

            printf("Parent Process exit!\n");

            _exit(1);
        }        
    }

    return 0;
}

4.4.2 waitpid 函数

函数文档

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

pid_t waitpid(pid_t pid, int *wstatus, int options);
  • 功能
    • **函数是 **Unix 和 **类 Unix 系统(Linux macOS FreeBSD)**中用于进程同步的一个重要系统调用,它的主要功能是等待指定子进程的状态发生变化,比如子进程终止、停止或继续执行等。
    • 可以认为是对子进程的监控,OB 模式
  • 参数解释
    • pid_t pid:
      • pid > 0: 指定当前函数监控的子进程 PID 号
      • pid == 0: 等待与调用进程属于同一进程组的任意子进程。
      • pid == -1 : 等待任意一个子进程,这种情况下 waitpid 的作用和 wait 函数类似。
      • pid < -1 : 等待进程组 ID 等于 pid 绝对值的任意子进程。例如实际参数为 -5555, 当前函数的作用是监控在进程组 5555 中的任何一个子进程操作。
    • int *wstatus:
      • 当前进程退出状态,或者当前工作状态。
    • options
      • 该参数是一个位掩码,用于指定 waitpid 函数的行为选项,常用的选项有:
      • WNOHANG:如果没有子进程发生状态变化,函数不阻塞,立即返回 0。
      • WUNTRACED:除了返回终止子进程的状态信息外,还会返回因信号而停止的子进程的状态信息。
      • WCONTINUED:返回那些因收到 SIGCONT 信号而恢复执行的子进程的状态信息。
  • 返回值解释
    • 大于 0:返回状态发生变化的子进程的进程 ID。
    • 等于 0:当使用 WNOHANG 选项且没有子进程状态发生变化时返回 0。
    • 等于 -1:
      • **表示出现错误,此时会设置 **errno 来指示具体的错误类型,常见错误有:
      • ECHILD:调用进程没有符合条件的子进程。
      • EINTR:在等待过程中被信号中断。
      • EINVALoptions 参数中包含了无效的标志。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define FORK_ERROR_FLAG -1
#define SON_PROCESS 0

int main(int argc, char const *argv[])
{
    pid_t pid;

    pid = fork();
    if (FORK_ERROR_FLAG == pid)
    {
        perror("Fork Failed!");
    }

    if (SON_PROCESS == pid)
    {
        int second = 5;

        while (second > 0)
        {
            printf("Son process running! Parent process PID %d, %d second left!\n", getppid(), second);
            second -= 1;
            sleep(1);
        }

        // _exit(3);
    }
    else
    {
        sleep(10);
        printf("Parent Process Running , PID : %d\n", getpid());
        /*
        pid_t waitpid(pid_t pid, int *wstatus, int options);

            WNOHANG Don't block waiting.
            WUNTRACED  Report status of stopped
            WCONTINUED Report continued child
        */
        int status = 0;
        pid_t child_pid = waitpid(-1, &status, WNOHANG);

        /*
        情况一:
            pid_t child_pid = waitpid(-1, &status, WNOHANG);
            非阻塞情况下,且当前程序中没有任何一个子进程发生状态改变,
            返回结果为 0,父进程后续代码中,没有阻塞,没有其他任务,当前
            父进程终止运行。
            【注意】如果父进程关闭,当前父进程所有的子进程都会交给进程 PID 为 1
                系统 init 初始化进程管理,最终销毁当前子进程使用的相关资源

        情况二:
            pid_t child_pid = waitpid(-1, &status, WUNTRACED);
            阻塞状态下,必须监控到任意一个子进程出现关闭情况,得到对应
            子进程相关信息.
        */

        if (child_pid > -1)
        {
            printf("child_pid : %d\n", child_pid);

            if (WIFEXITED(status))
            {
                // 如果当前进程关闭,WIFEXITED 返回值非 0
                printf("WEXITSTATUS(status) : %d\n", WEXITSTATUS(status));
            }
        }
        else
        {
            perror("waitpid!");
        }
    }

    return 5;
}

4.5 进程退出函数

函数原型

  • _exit
    • 头文件:#include <unistd.h>
    • 原型:void _exit(int status);
    • 系统调用函数
  • exit
    • 头文件:#include <stdlib.h>
    • 原型:void exit(int status);
    • C 语言库函数
4.6 进程退出清理函数 atexit

函数文档

#include <stdlib.h>
int atexit(void (*func)(void));
  • 函数功能:
    • C 语言库函数,用于在正常结束进程的情况下,执行目标已注册函数。
    • **当调用 atexit 函数时,会把指定的函数 func 注册到一个特殊的函数列表(栈结构)中。在程序正常终止(比如通过 exit 函数、从 main 函数返回)**时,系统会按照与注册顺序相反【底层栈结构 FILO】的次序来调用这些已注册的函数。
    • 不过要注意,若程序是因为调用 abort 函数、接收到未处理的信号等异常情况而终止,那么这些已注册的函数不会被调用。
  • 参数列表:
    • func: 函数指针,要求函数是无参数无法返回值函数。注册顺序和执行顺序相反【FILO】
  • 返回值类型
    • 若函数注册成功,atexit 会返回 0。
    • 若注册失败,就会返回非零值。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

void fun1(void);
void fun2(void);
void fun3(void);

// int fun4(int n);

int main(int argc, char const *argv[])
{
    int ret = atexit(fun1);
    printf("Function One atexit result : %d\n", ret);

    ret = atexit(fun2);
    printf("Function Two atexit result : %d\n", ret);

    ret = atexit(fun3);
    printf("Function Three atexit result : %d\n", ret);

    // ret = atexit(fun4);
    // printf("Function Parameter type is int,  atexit result : %d\n", ret);

    printf("Program exit 5 seconds later!\n");
    sleep(5);

    return 0;
}

void fun1(void) 
{
    printf("Function One Running...\n");
}
void fun2(void) 
{
    printf("Function Two Running...\n");
}
void fun3(void) 
{
    printf("Function Three Running...\n");
}

// int fun4(int n)
// {
//     printf("Parameter Type : int\n");
// }
4.7 进程创建函数 vfork

函数文档

#include <unistd.h>

pid_t vfork(void);
  • 功能:
    • **创建一个子进程。**重点是子进程对应内存空间,使用的实际上是父进程内存空间
  • 返回值类型
    • 在子进程中,返回值结果为 0
    • 在父进程中,返回值结果是父进程创建的子进程 PID
    • 如果创建失败返回 -1
  • 【重点】fork 和 vfork 区别
    • vfork 优先保证子进程执行,在子进程调用 【exec 族】或者【exit 函数】之后,父进程才可以正常执行。
    • vfork 创建的子进程使用的内存空间来自于父进程,在使用时需要注意内存空间冲突问题。
    • 【注意】有且只有子进程执行 【exec 族】相关函数,子进程才可以申请自行的进程执行内存空间,与父进程无关

vfork 创建进程 子进程优先执行

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

/*
当前程序证明,vfork 优先执行子进程
*/
int main(int argc, char const *argv[])
{
    pid_t pid = 0;

    pid = vfork();

    if (-1 == pid)
    {
        perror("vfork failed!\n");
    }

    if (0 == pid)
    {
        int count = 5;

        while (count > 0)
        {
            printf("Son process running! Exit in %d second\n", count);
            sleep(1);
            count -= 1;
        }
        /*
        exit
        _exit
        */
       _exit(0);
    }
    else
    {
        int count = 5;

        while (count > 0)
        {
            printf("Parent process running! Exit in %d second\n", count);
            sleep(1);
            count -= 1;
        }

        exit(0);
    }
    
    return 0;
}

vfork 子进程和父进程内存空间一致

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

/*
vfor 子进程和进程内存空间问题
*/

/*
全局变量
*/
int global_var = 10;

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

    pid = vfork();

    /*
    局部变量,在当前函数执行,或代码块执行过程中有效。
    */
    int num = 0;

    if (-1 == pid)
    {
        perror("vfork failed!\n");
    }

    if (0 == pid)
    {
        int count = 5;

        while (count > 0)
        {
            printf("Son process running! Exit in %d second\n", count);

            num += 1;
            global_var += 1;

            sleep(1);
            count -= 1;
        }

        printf("&num : %p\n", &num);
        /*
        exit
        _exit
        */
       _exit(0);
    }
    else
    {
        int count = 5;

        while (count > 0)
        {
            printf("Parent process running! Exit in %d second\n", count);
            sleep(1);
            count -= 1;
        }

        /*
        num 结果为 0,因为 num 是一个函数内部的局部变量,进程执行完毕之后,对应使用局部变量
        被系统销毁了,在父进程中使用 num 可以认为是父进程自行使用的 num 局部变量,但是有可能
        出现子进程和父进程局部变量地址一致

        global_var 全局变量,可以证明,子进程和父进程使用的内存空间是一致的,因为在子进程中
        对 global_var 进行了修改,在父进程中同样有效。
        【全局变量】可以作为子进程和父进程数据交互的方式。
        */
        printf("in parent process num = %d, global_var = %d\n", num, global_var);
        printf("&num : %p\n", &num);

        exit(0);
    }
    
    return 0;
}
4.8 进程交换函数 exec 函数族

进程执行 exec 函数族相关函数,可以认为当前进程已关闭,执行目标 exec 对应的进程内容,同时会申请 exec 执行目标进程所需的内存空间。

函数文档

#include <unistd.h>

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

参数说明

  • path:要执行的程序的路径名。
  • arg:传递给新程序的第一个参数,通常是程序名本身。后续可以跟可变数量的参数,以 NULL 结尾。
  • 例如: execl("/bin/ls", "-al", NULL);
#include <unistd.h>

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

int main(int argc, char const *argv[])
{
    // execl("/bin/ls", "ls", "-a", "-l", "-g", NULL);
    execl("./fork2.out", "./fork2.out", NULL);
    /*ls
    "/bin/ls" 执行程序的目标路径,可以是相对路径,也可以是绝对路径

    "ls", "-a", "-l" 当前执行程序对应参数,也可以认为是命令行中的内容
    */

    return 0;
}

函数文档

#include <unistd.h>
int execv(const char *path, char *const argv[]);

参数说明

  • path:要执行的程序的路径名。
  • argv:一个以 NULL 结尾的字符串数组,包含传递给新程序的参数。
#include <unistd.h>

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

int main(int argc, char const *argv[])
{
    /*
    用户执行程序对应的命令为 ./ls -a -l
    数组内容 argv = {"./ls", "-a", "-l"};

    三选一
    1. memcpy
    2. strtok
    3. sscanf
        将 "./ls" ==> "ls"
    
    最终效果是 当前程序编译采用的命令为 gcc 12-exev.c -o ls
    执行
        ./ls -a -l
    得到效果和 ls -al 一致
    */
    char * argument_array[4] = {"ls", "-l" , "-a", NULL};
    execv("/bin/ls", argument_array);

    return 0;
}