Linux系统 进程

Linux系统 进程

进程

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

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

私有地址空间

进程的私有地址空间包括

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

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

用户模式和内核模式

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

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

上下文切换

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

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

进程控制

获取进程 ID

C 复制代码
#include <sys/types.h>
#include <unistd.h>

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

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

创建和终止进程

C 复制代码
#include <stdlib.h>

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

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

C 复制代码
#include <sys/types.h>
#include <unistd.h>

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

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

C 复制代码
// 使用 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);
}
shell 复制代码
linux> ./fork
parent:x=0
child :x=2

回收子进程

C 复制代码
#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 函数

C 复制代码
#include <sys/types.h>
#include <sys/wait.h>

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

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

让进程休眠

C 复制代码
#include <unistd.h>

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

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

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

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

加载并运行程序

C 复制代码
#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的栈帧。

操作环境数组

C 复制代码
#include <stdlib.h>

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

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

C 复制代码
#include <stdlib.h>

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

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

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

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

系统调用错误处理

C 复制代码
void unix_error(char *msg)
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(0);
}
C 复制代码
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来加载并执行该命令,父进程根据命令是否在后台运行来决定是否等待子进程结束。

C 复制代码
#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 是有缺陷的,因为它并不回收它的后台子进程。修改这个缺陷就要求使用信号,详见信号部分。

相关推荐
mit6.8242 分钟前
Ubuntu 系统下性能剖析工具: perf
linux·运维·ubuntu
鹏大师运维3 分钟前
聊聊开源的虚拟化平台--PVE
linux·开源·虚拟化·虚拟机·pve·存储·nfs
watermelonoops11 分钟前
Windows安装Ubuntu,Deepin三系统启动问题(XXX has invalid signature 您需要先加载内核)
linux·运维·ubuntu·deepin
滴水之功1 小时前
VMware OpenWrt怎么桥接模式联网
linux·openwrt
ldinvicible1 小时前
How to run Flutter on an Embedded Device
linux
嵌入式科普2 小时前
十一、从0开始卷出一个新项目之瑞萨RA6M5串口DTC接收不定长
c语言·stm32·cubeide·e2studio·ra6m5·dma接收不定长
A懿轩A2 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
YRr YRr2 小时前
解决Ubuntu 20.04上编译OpenCV 3.2时遇到的stdlib.h缺失错误
linux·opencv·ubuntu
认真学习的小雅兰.2 小时前
如何在Ubuntu上利用Docker和Cpolar实现Excalidraw公网访问高效绘图——“cpolar内网穿透”
linux·ubuntu·docker
zhou周大哥2 小时前
linux 安装 ffmpeg 视频转换
linux·运维·服务器