Bootstrap

Linux系统 进程

进程

异常是允许操作系统内核提供进程(process)概念的基本构造块,进程是计算机科学中最深刻、最成功的概念之一。异常控制流与进程有密切的关系,因为它为进程的多任务运行、异常处理和资源管理提供了基础。

进程是操作系统对正在运行的程序的一种抽象。它为每个程序提供了一种假象,仿佛每个程序都在独占地使用处理器、主存和 I/O 设备,而实际上系统中可能同时存在多个进程并发执行,操作系统负责管理和协调这些进程对硬件资源的共享。

私有地址空间

在这里插入图片描述

进程的私有地址空间包括

  • 代码区:存放程序代码,通常是只读的,以防止程序意外修改自身指令。
  • 数据区:包含已初始化的全局和静态变量。
  • 堆区:从低地址向高地址增长,用于动态内存分配,如程序运行过程中根据需要创建的数据结构。
  • 栈区:从高地址向低地址增长,用于存放函数调用相关信息,如局部变量、函数参数、返回地址等。

这种布局有助于组织和管理进程的数据和指令存储,并且通过地址空间的隔离保证了进程间的独立性和安全性,防止一个进程意外访问或修改另一个进程的数据。

用户模式和内核模式

在这里插入图片描述

在用户模式下,进程只能访问自己的虚拟内存空间,无法直接操作硬件或执行特权指令。而在内核模式下,进程具有完全的权限,可以访问整个内存空间并与硬件直接交互,操作系统的核心功能大多在内核模式下执行。

进程从用户模式切换到内核模式通常由系统调用中断异常触发。当进程请求操作系统服务时,它会通过系统调用触发模式切换。系统调用通过陷阱指令使得进程从用户模式转入内核模式,操作系统处理完请求后,再返回用户模式继续执行。

上下文切换

进程上下文包含了进程执行所需的所有信息,这些信息完整地描述了进程在某一时刻的执行状态,当进程被暂停或切换时,操作系统需要保存这些上下文信息,以便在下次恢复执行时能够准确地回到之前的执行状态。

  • 程序计数器(PC)的值:指示下一条要执行的指令地址。
  • 寄存器的值:包括通用寄存器、栈指针、程序状态字寄存器,用于存储进程当前的计算状态和临时数据。
  • 页表:用于将虚拟地址转换为物理地址,实现内存管理。
  • 内核栈:用于保存进程在内核态执行时的函数调用信息和临时数据。

进程控制

获取进程 ID

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

pid_t getpid(void);
pid_t getppid(void);
// 返回:调用者或其父进程的 PID。

getpidgetppid 函数返回一个类型为 pid_t 的整数值,在 Linux 系统上它在 types.h 中被定义为 int。

创建和终止进程

#include <stdlib.h>

void exit(int status);
// 该函数不返回。

exit 函数以 status 退出状态来终止进程(或返回一个整数值)。

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

pid_t fork(void);
// 返回:子进程返回 0,父进程返回子进程的 PID,如果出错,则为 -1。

父进程调用fork后,操作系统会创建一个新的进程控制块(PCB)和地址空间等资源给子进程,然后将父进程的上下文复制到子进程中。之后,父进程和子进程从fork函数返回后开始独立执行,虽然它们从相同的代码位置继续执行,但由于返回值不同,可以通过判断返回值来执行不同的代码分支,例如父进程可以在fork后继续执行其他任务,同时等待子进程完成某些操作,而子进程可以执行特定于子进程的任务,如执行另一个程序等。

// 使用 fork 创建一个新进程
int main()
{
    pid_t pid;
    int x = 1;

    pid = Fork();
    if (pid == 0) { /* Child */
        printf("child : x=%d\n", ++x);
        exit(0);
    }
    
    /* Parent */
    printf("parent: x=%d\n", --x);
    exit(0);
}

在这里插入图片描述

linux> ./fork
parent:x=0
child :x=2

回收子进程

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

pid_t waitpid(pid_t pid, int *statusp, int options);
// 返回:如果成功,则为子进程的 PID,如果 WNOHANG,则为 0,如果其他错误,则为 -1。

pid:指定要等待的子进程的 PID(进程标识符)。

  • 如果 pid > 0,则等待进程 ID 为 pid 的子进程结束。
  • 如果 pid == 0,则等待与当前进程同属一个进程组的任何子进程结束。
  • 如果 pid == -1,则等待任何子进程结束(类似于 wait)。
  • 如果 pid < -1,则等待进程组 ID 为 pid 的子进程结束(即进程组内的任何子进程)。

status:指向整型变量的指针,用于存储子进程的退出状态。如果该参数为 NULL,则不保存子进程的状态。

options:控制等待行为的选项,常见的选项有:

  • WNOHANG:非阻塞模式,如果没有子进程退出,waitpid 立即返回而不是阻塞。
  • WUNTRACED:在子进程停止(但没有终止)时也返回,即使它没有结束。
  • WCONTINUED:如果子进程因信号而暂停,可以在它继续运行时通知父进程。
  • WNOHANG | WUNTRACED:立即返回,如果等待集合中的子进程都没有被停止或终止,则返回值为 0;如果有一个停止或终止,则返回值为该子进程的 PID。

子进程的退出状态:如果 status 参数不为 NULLwaitpid 会将子进程的退出状态存储在 status 指向的变量中。退出状态可以通过以下宏来解析:

  • WIFEXITED(status):如果子进程正常退出,返回真。
  • WEXITSTATUS(status):返回子进程的退出码。只有在 WIFEXITED() 返回为真时,才会定义这个状态。
  • WIFSIGNALED(status):如果子进程因为未捕捉到的信号退出,返回真。
  • WTERMSIG(status):返回导致子进程终止的信号号码。
  • WIFSTOPPED(status):如果子进程被信号暂停,返回真。
  • WSTOPSIG(status):返回导致子进程暂停的信号号码。
  • WIFCONTINUED(status):如果子进程因接收到继续信号(如 SIGCONT)而恢复,返回真。

错误条件:如果调用进程没有子进程,那么 waitpid 返回 -1,并且设置 errno 为 ECHILD。如果 waitpid 函数被一个信号中断,那么它返回 -1,并设置 errno 为 EINTR。

wait 函数

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

pid_t wait(int *statusp);
// 返回:如果成功,则为子进程的 PID,如果出错,则为 -1。

调用 wait(&status) 等价于调用 waitpid(-1, &status, 0)

让进程休眠

#include <unistd.h>

unsigned int sleep(unsigned int secs);
// 返回:还要休眠的秒数。

int pause(void);
// 总是返回 -1。

sleep函数将一个进程挂起一段指定的时间。

pause函数让调用函数休眠,直到该进程收到一个信号。

加载并运行程序

#include <unistd.h>

int execve(const char *filename, const char *argv[],
           const char *envp[]);
// 如果成功,则不返回,如果错误,则返回 -1。

execve 是一个用于启动新程序的系统调用,它是 UNIX 和 Linux 系统中执行程序的重要机制。通过 execve,当前进程可以加载并执行一个新的程序文件。执行该系统调用后,当前进程的地址空间(包括代码、数据、堆栈等)将被替换为新程序的内容,原进程的代码不再执行。

  • pathname:要执行的程序的路径。它是一个以 null 结尾的字符串,指定了要执行的文件的绝对路径或相对路径。
  • argv:一个指向参数数组的指针,用于传递给新程序的命令行参数。它是一个数组,其中第一个元素通常是程序的名称,接下来的元素是传递给程序的各个参数。数组的最后一个元素必须是 NULL
  • envp:一个指向环境变量数组的指针。它是一个包含环境变量的数组,每个环境变量是一个以 = 分隔的字符串。数组的最后一个元素必须是 NULL

int main(int argc, char *argv[], char *envp[]);

  • argc 是一个整数,表示命令行中传递给程序的参数的数量。它的值至少为 1,因为 argv[0] 始终是程序的名称或路径。
  • argv 是一个指向字符串数组的指针,数组中的每个元素是一个指向以 null 字符结尾的字符串的指针。这些字符串对应于命令行中传递给程序的参数。
  • envp 是一个指向环境变量数组的指针。每个环境变量是一个以 null 字符结尾的字符串,它的格式通常是 KEY=VALUE,例如 PATH=/usr/bin

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

main 开始执行时,用户栈的组织结构下图所示。从栈底(高地址)往栈顶(低地址)依次看。首先是参数和环境字符串。栈往上紧随其后的是以 null 结尾的指针数组,其中每个指针都指向栈中的一个环境变量字符串。全局变量 environ 指向这些指针中的第一个 envp[0] 紧随环境变量数组之后的是以 null 结尾的 argv[] 数组,其中每个元素都指向栈中的一个参数字符串。在栈的顶部是系统启动函数 libc_start_main的栈帧。

在这里插入图片描述

操作环境数组

#include <stdlib.h>

char *getenv(const char *name);
// 返回:若存在则为指向 name 的指针,若无匹配的,则为 NULL。

getenv 用于从环境变量中获取指定变量的值。环境变量是操作系统用于存储关于当前进程的配置信息的键值对,比如用户的主目录、系统路径等。

#include <stdlib.h>

int setenv(const char *name, const char *newvalue, int overwrite);
// 返回:若成功则为 0,若错误则为 -1。

void unsetenv(const char *name);
// 返回:无。

setenv 用于设置(或修改)一个环境变量的值。如果该环境变量已存在,它将被更新;如果不存在,它会被创建。

unsetenv 用于删除指定的环境变量。调用该函数后,该环境变量在当前进程的环境中将不再可用。

系统调用错误处理

void unix_error(char *msg)
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(0);
}
pit_t Fork()
{
    pit_t pid;
    if ((pid = fork()) < 0)
        unix_error("Fork error");
    return pid;
}

Unix 系统级函数出错时通常返回 -1 并设置 errno,上面是对系统调用的错误处理封装,下面的部分中都使用错误处理包装函数。它们能够保持代码示例简洁,而又不会给你错误的假象,认为允许忽略错误检査。包装函数定义在一个叫做 csapp.c 的文件中,它们的原型定义在一个叫做 csapp.h 的头文件中;可以从 CS:APP 网站上在线地得到这些代码。

利用fork和execve运行程序

实现了一个简单的命令行解释器(极简的 Shell),它可以读取用户输入的命令行,解析命令并执行。如果命令是内置命令(如quit),则直接执行相应操作;如果是外部命令,则通过fork创建子进程,在子进程中使用execve来加载并执行该命令,父进程根据命令是否在后台运行来决定是否等待子进程结束。

#include "csapp.h"
#define MAXARGS 128

// Evaluate a command line
void eval(char *cmdline);
// Parse the command line and build the argv array
int parseline(char *buf, char **argv);
// If first arg is a builtin command, run it and return true
int builtin_command(char **argv);

int main()
{
    char cmdline[MAXLINE];   
    while (1) {
        // Read
        printf("> ");
        Fgets(cmdline, MAXLINE, stdin);
        if (feof(stdin))
            exit(0);
        // Evaluate
        eval(cmdline); 
    }
    return 0;
}

void eval(char *cmdline)
{
    char *argv[MAXARGS]; // Argument list execve()
    char buf[MAXLINE];   // Holds modified command line
    int bg;              // Should the job run in bg or fg?
    pid_t pid;           // Process id

    strcpy(buf, cmdline);
    bg = parseline(buf, argv); 
    if (argv[0] == NULL)
        return; // Ingore empty lines
    
    if (!(builtin_command(argv))) {
        if ((pid = Fork()) == 0) // Child runs user job
        {
            if (execve(argv[0], argv, environ) < 0)
            {
                printf("%s Commmand not found.\n", argv[0]);
                exit(0);
            }
        }
        // Parent waits for foreground job to terminate
        if (!bg)
        {
            int status;
            if (waitpid(pid, &status, 0) < 0)
                unix_error("waitfg: wait error");
        }
        else
            printf("%d %s", pid, cmdline);
    }
}

int parseline(char *buf, char **argv)
{
    char *delim; // Points to first space delimiter
    int argc;    // Number of args
    int bg;      // Background jobs?

    buf[strlen(buf) - 1] = ' '; // Replace trailing '\n' with space
    while (*buf && (*buf == ' ')) // Ignore leading space
        buf++;

    // Build the argv list
    argc = 0;
    while ((delim = strchr(buf, ' '))) {
        *delim = '\0';
        argv[argc++] = buf;
        buf = delim + 1;
        while (*buf && (*buf == ' '))
            buf++;
    }
    argv[argc] = NULL;
    if (argc == 0) // Ignore blank line
        return 1;
    
    // Should the job run in the background?
    if (bg = ((*argv[argc-1]) == '&'))
        argv[--argc] = NULL;
    return bg;
}

int builtin_command(char **argv)
{
    if (!strcmp(argv[0], "quit")) // quit command
        exit(0);
    if (!strcmp(argv[0], "&"))    // Ignore singleton
        return 1;
    return 0; // Not a builtin command
}

在这里插入图片描述

注意,这个简单的 shell 是有缺陷的,因为它并不回收它的后台子进程。修改这个缺陷就要求使用信号,详见信号部分。

;