解码程序与进程

程序与进程基础概念

程序

程序是一系列有序指令的集合,用于告诉计算机完成特定操作或解决问题。

  • 编程语言发展:机器语言→汇编语言(指令集不兼容)→高级语言(C、C++、Python 等,提高开发效率)。
  • 程序的存在形式:以源文件(如.c.py)存储在磁盘中,是静态文本,需经编译转换为可执行文件才能运行。

进程

进程是程序在处理器上的一次执行过程,是操作系统分配资源的基本单位(线程是调度的最小单位,进程包含线程)

  • 动态性:程序运行时从外存加载到内存,系统分配资源(内存、CPU),从静态变为动态。
  • 核心区别:
    • 程序是 “静态文本”,进程是 “动态执行过程”;
    • 一个程序可对应多个进程(如多次打开同一软件)。

冯诺依曼结构

计算机硬件遵循冯诺依曼结构,由五大核心组件组成:

image

  • 控制器:指挥程序运行,协调各组件工作。
  • 运算器:执行算术运算和逻辑运算。
  • 存储器:存放程序和数据(内存、外存)。
  • 输入设备:将外部信息转换为计算机可识别形式(如键盘、鼠标)。
  • 输出设备:将计算结果转换为用户可理解形式(如显示器、打印机)。

程序编译流程(GCC 编译器)

C 语言程序需经 4 个阶段编译生成可执行文件,GCC 是 Linux 默认 C/C++ 编译器,核心命令及流程如下:

image

虚拟内存

  • 32 位系统中,每个进程默认拥有 4G 虚拟内存空间,分为内核空间(1G,高地址)和用户空间(3G,低地址)。
  • 虚拟内存通过 MMU(内存管理单元)映射到物理内存,进程间内存空间独立,互不干扰。

编译整体流程

image

各阶段详细说明

预处理阶段

  • 作用:处理源文件中所有#开头的预处理指令,删除注释,添加调试信息。
    • #include:将包含的头文件内容直接拷贝到源文件中。
    • #define:展开所有宏定义。
    • #if:执行条件编译,保留符合条件的代码。
  • GCC 命令:gcc -E xxx.c -o xxx.i
    • 参数E:仅执行预处理,不进行后续编译、汇编、链接。
    • 参数o:指定输出文件名为xxx.i(预处理文件)。

编译阶段

  • 作用:对预处理后的.i文件进行词法、语法分析,生成对应硬件平台的汇编文件(如 X86 平台用 GCC,ARM 平台用交叉编译器arm-linux-gcc)。
  • GCC 命令:gcc -S xxx.i -o xxx.s
    • 参数S:编译到汇编语言,不进行汇编和链接。
    • 输出文件xxx.s:包含汇编指令的文本文件。

汇编阶段

  • 作用:调用汇编器as,将汇编文件.s翻译为机器指令,生成可重定位目标文件(.o)。
  • GCC 命令:gcc -c xxx.s -o xxx.o
    • 参数c:编译、汇编到目标代码,不进行链接。
    • 输出文件xxx.o:ELF 格式的可重定位文件,包含机器指令但未关联系统库。

链接阶段

  • 作用:将目标文件.o与系统标准库(如 C 库libc.so)、其他依赖的.o文件结合,重定位函数地址,生成可执行文件。
  • GCC 命令:gcc xxx.o -o xxx -lc -lgcc
    • 参数lc:链接标准 C 库(libc,可省略,GCC 默认链接)。
    • 参数lgcc:链接 GCC 基础库(可省略)。
    • 输出文件xxx:ELF 格式的可执行文件,可直接运行。

GCC 常用参数

参数 作用说明
--help 查看 GCC 所有参数用法
--version 显示 GCC 版本信息
-Wall 开启所有警告提示(推荐开发时使用)
-g 添加调试信息,支持 GDB 调试
-O2 开启二级优化,平衡编译速度和程序运行效率
-Wl,<选项> 将后续选项传递给链接器(如-Wl,-rpath=.

ELF 文件结构(目标文件与可执行文件)

Linux 系统中,目标文件(.o)和可执行文件均为 ELF(Executable Linkable Format)格式,核心是按 “节(Section)” 存储不同类型数据,部分节也称为 “段(Segment)”。

image

核心节的作用

节名 作用说明
.text 代码段,存放编译后的机器指令(只读)
.data 数据段,存放已初始化的全局变量和静态局部变量
.bss 未初始化数据段,存放未初始化的全局变量和静态局部变量(编译时不分配空间,运行时分配)
.rodata 只读数据段,存放字符串常量、const全局变量(只读)
.symtab 符号表,存放函数、变量的名称和地址信息
.strtab 字符串表,存储 ELF 文件中用到的所有字符串(如节名、变量名)
.comment 存放编译器版本信息(如GCC:(GNU)4.2.0
.debug 调试信息,供 GDB 调试使用(需编译时加-g

目标文件与可执行文件的区别

  • 目标文件(.o):可重定位文件,仅包含当前源文件的指令和数据,未关联系统库,需链接后才能运行。
  • 可执行文件:已完成链接,包含完整的指令、数据和库依赖信息,可直接被操作系统加载运行。
  • 查看工具:file 文件名(查看文件格式)、objdump -h 文件名(查看节信息)、objdump -D 文件名(反汇编可执行文件)。

示例:查看目标文件节信息

# 编译生成目标文件
gcc -c demo.c -o demo.o
# 查看demo.o的节信息
objdump -h demo.o

进程核心知识

进程的特征

  • 动态性:进程是程序的执行过程,会经历创建、运行、暂停、终止等状态变化。
  • 并发性:多个进程同时存在于内存中,交替占用 CPU 运行(宏观并行,微观串行),提高资源利用率。
  • 独立性:进程是独立运行、资源分配和调度的基本单位,拥有独立的内存空间。
  • 异步性:进程以不可预知的速度推进,受 CPU 调度、资源竞争等因素影响。

进程的组成

进程由三部分组成,缺一不可:

  • 进程控制块(PCB):操作系统为每个进程分配的内存区域,记录进程的关键信息(Linux 中用struct task_struct结构体,定义在sched.h头文件中)。
  • 代码段:进程对应的程序指令(从可执行文件加载,只读)。
  • 数据段:进程运行时使用的数据(全局变量、局部变量、中间结果等,可读可写)。

PCB 中的关键信息

  • 进程标识符(PID):系统分配的唯一 ID,用于区分不同进程(Linux 中 PID 是用户操作进程的接口)。
  • 进程当前状态:如就绪态、运行态等,是 CPU 调度的依据。
  • 资源占用信息:进程占用的内存大小、CPU 时间、打开的文件等。
  • 父进程 PID(PPID):记录创建当前进程的父进程 ID。

进程的五种基本状态及转换

五种状态定义

image

  • 创建态:进程正在被创建,系统分配 PCB 和初始资源,未进入就绪态。
  • 就绪态:已获得除 CPU 外的所有资源,等待 CPU 调度(“万事俱备,只欠 CPU”)。
  • 运行态:已获得 CPU 资源,正在执行指令。
  • 阻塞态:进程执行中因某事件(如sleepread)无法继续,暂时放弃 CPU,等待事件完成。
  • 结束态:进程完成任务或异常终止,释放资源(若未释放资源则变为僵尸态)。

状态转换规则

  • 创建态 → 就绪态:进程创建完成,资源分配完毕。
  • 就绪态 → 运行态:CPU 调度器选中该进程,分配 CPU 资源。
  • 运行态 → 就绪态:时间片用完或有更高优先级进程进入就绪态,当前进程放弃 CPU。
  • 运行态 → 阻塞态:进程触发阻塞事件(如等待 I/O、调用sleep)。
  • 阻塞态 → 就绪态:阻塞事件完成(如 I/O 结束、sleep超时)。
  • 运行态 → 结束态:进程正常退出(return 0)或异常终止(如信号终止)。
  • 结束态 → 僵尸态:进程终止但资源未被回收(需父进程调用wait()回收)。

Linux 中进程状态查看

  • 命令:ps -aux 或 ps -ef(查看所有进程信息)。
  • 状态标识(STAT 列):
    • R:运行态或就绪态(在 CPU 运行队列中)。
    • S:可中断睡眠(阻塞态,等待事件完成)。
    • D:不可中断睡眠(通常等待 I/O)。
    • T:暂停态(被信号或调试器停止)。
    • Z:僵尸态(进程终止但资源未回收)。
    • Ss:主进程处于睡眠态,子进程运行(如systemd)。

进程控制(创建、撤销、执行)

进程控制由操作系统内核实现,核心通过系统调用接口完成,Linux 中常用接口如下:

进程树与 systemd 进程

  • Linux 中所有进程构成进程树,根进程是systemd(PID=1,系统守护进程)。

  • 所有用户进程都是systemd的子孙进程,可通过pstree查看进程树结构。

    image

进程创建:fork ()

fork()是 Linux 内核提供的系统调用,用于在当前进程(父进程)中创建一个新进程(子进程)。

函数原型

#include <sys/types.h>
#include <unistd.h>
/**
 * 创建子进程(子进程复制父进程的代码段、数据段、堆栈段)
 * @return 父进程中返回子进程的PID(正整数);子进程中返回0;失败时父进程返回-1,errno设置错误码
 * @note 子进程与父进程运行在独立内存空间,写操作互不影响(读时共享,写时复制)
 *       子进程不继承父进程的内存锁、定时器、未完成的异步I/O操作
 *       子进程的PID唯一,PPID等于父进程PID
 *       父进程与子进程的执行顺序由CPU调度器决定,无固定优先级
 */
pid_t fork(void);

使用示例

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[]) {
    // 父进程的数据段变量
    int num = 10;

    // 创建子进程,调用fork时子进程已复制父进程资源
    pid_t child_pid = fork();

    // 通过fork返回值区分父进程和子进程
    if (child_pid > 0) {
        // 父进程:返回值为子进程PID(正整数)
        num += 5;
        printf("我是父进程 | PID: %d | 子进程PID: %d | num: %d\n", getpid(), child_pid, num);
    } else if (child_pid == 0) {
        // 子进程:返回值为0
        num -= 5;
        printf("我是子进程 | PID: %d | 父进程PID: %d | num: %d\n", getpid(), getppid(), num);
    } else {
        //  fork失败:父进程返回-1
        perror("fork创建子进程失败");
        return -1;
    }

    return 0;
}

关键说明

  • getpid():获取当前进程的 PID。
  • getppid():获取当前进程的父进程 PID。
  • 子进程复制父进程的资源,但写操作会触发 “写时复制”(Copy-On-Write),即子进程会创建独立副本,不影响父进程数据。

进程撤销:wait () 与 waitpid ()

进程终止后需回收资源,否则会变为僵尸态(占用 PID 和内存)。wait()waitpid()用于父进程等待子进程状态变化并回收资源。

wait () 函数原型

#include <sys/types.h>
#include <sys/wait.h>
/**
 * 父进程等待子进程状态变化,回收子进程资源
 * @param wstatus 指向存储子进程退出状态的整数(NULL表示不关心退出状态)
 * @return 成功返回被回收子进程的PID;失败返回-1(如无子女进程)
 * @note  若子进程已终止(僵尸态),函数立即返回并回收资源
 *        若子进程未终止,函数阻塞直到子进程状态变化(终止、暂停、恢复)
 *        仅回收第一个变为僵尸态的子进程,无法指定回收目标
 */
pid_t wait(int *wstatus);

waitpid () 函数原型

#include <sys/types.h>
#include <sys/wait.h>
/**
 * 指定回收目标子进程,功能更灵活(推荐使用)
 * @param pid 回收目标标识:
 *            - pid > 0: 回收PID等于该值的子进程
 *            - pid = -1:回收任意子进程(等同于wait())
 *            - pid = 0: 回收与父进程同进程组的任意子进程
 *            - pid < -1:回收进程组ID等于pid绝对值的任意子进程
 * @param wstatus 存储子进程退出状态的指针(NULL表示不关心)
 * @param options 回收选项(可组合使用):
 *                - WNOHANG:非阻塞模式,无僵尸子进程时立即返回0
 *                - WUNTRACED:子进程暂停时也返回状态
 *                - WCONTINUED:子进程被SIGCONT信号恢复时返回状态
 * @return 成功返回被回收子进程的PID;WNOHANG模式下无僵尸子进程返回0;失败返回-1
 * @note 可精准控制回收哪个子进程,避免wait()的盲目等待
 *       子进程退出状态需通过宏解析(如WIFEXITED、WEXITSTATUS)
 */
pid_t waitpid(pid_t pid, int *wstatus, int options);

子进程退出状态解析宏

宏名 作用说明
WIFEXITED(wstatus) 判断子进程是否正常退出(return 或 exit),正常返回非 0
WEXITSTATUS(wstatus) 获取正常退出的子进程返回值(仅 WIFEXITED 为真时有效)
WIFSIGNALED(wstatus) 判断子进程是否被信号终止(如 kill -9),是返回非 0
WTERMSIG(wstatus) 获取终止子进程的信号编号(仅 WIFSIGNALED 为真时有效)

使用示例

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char const *argv[]) {
    pid_t child_pid = fork();

    if (child_pid > 0) {
        // 父进程:等待子进程终止并回收资源
        int wstatus;
        // 阻塞等待PID为child_pid的子进程,关心退出状态
        pid_t reaped_pid = waitpid(child_pid, &wstatus, 0);

        if (reaped_pid == child_pid) {
            // 解析子进程退出状态
            if (WIFEXITED(wstatus)) {
                printf("子进程正常退出 | 退出码: %d\n", WEXITSTATUS(wstatus));
            } else if (WIFSIGNALED(wstatus)) { // kill -9 child_pid
                printf("子进程被信号终止 | 信号编号: %d\n", WTERMSIG(wstatus));
            }
        }
    } else if (child_pid == 0) {
        // 子进程:执行任务后退出
        printf("子进程运行中 | PID: %d\n", getpid());
        sleep(10); // 模拟任务执行
        exit(3); // 正常退出,返回码3
    } else {
        perror("fork失败");
        return -1;
    }

    return 0;
}

僵尸态进程处理

  • 僵尸态(Z):进程终止但资源未被父进程回收,由内核维护。
  • 处理方法:
    • 父进程调用wait()waitpid()回收子进程资源。
    • 父进程终止后,僵尸态子进程会被systemd(PID=1)接管并回收。
    • 若父进程长期不回收,可通过kill -9 父进程PID终止父进程,间接回收僵尸进程。
    • 子进程主动告知父进程前来收尸
      • 子进程在进入僵尸态时,会自动向父进程发送信号SIGCHILD,而父进程可以利用异步信号响应函数来及时处理这些僵尸子进程。

        void cleanup(int sig)
        {
            // 僵尸子进程会被自动清除
            wait(NULL);
        }
        int main()
        {
            // 在产生子进程之前,准备好处理它们的SIGCHILD信号
            signal(SIGCHLD, cleanup);
            // 子进程退出,成为僵尸进程
            pid_t child_pid = fork();
            if(child_pid  == 0)   return 0;
            else if(child_pid > 0) {
        		    // 父进程干自己的活,无需关注子进程
        		    while(1)
                pause();
            }
            return 0;
        }
        

进程执行:exec 函数族与 system ()

子进程创建后默认复制父进程的代码段,若需让子进程执行新程序,可使用exec函数族或system()

exec 函数族核心特性

  • 作用:替换当前进程的代码段、数据段和堆栈段,执行新程序(进程 PID 不变,仅内容替换)。
  • 命名规则:前缀exec后接字母,代表参数传递方式和功能:
    • l(list):参数以列表形式传递,最后必须以(char *)NULL结束。
    • v(vector):参数以字符串数组形式传递,数组末尾必须是NULL
    • p(path):自动搜索环境变量PATH中的路径,无需指定程序完整路径。
    • e(environment):自定义环境变量,需传递环境变量数组。

常用 exec 函数:execl () 与 execvp ()

execl () 函数原型与示例

#include <unistd.h>
/**
 * 以列表形式传递参数,执行指定程序(需完整路径)
 * @param pathname 程序完整路径(如"/bin/ls")
 * @param arg 第一个参数为程序名(惯例),后续为命令行参数,最后以(char *)NULL结束
 * @return 执行成功无返回(程序已替换);失败返回-1,errno设置错误码
 * @note 若pathname不含"/",不会搜索PATH(需用execlp())
 *       参数列表必须以NULL终止,否则会导致内存访问错误
 */
int execl(const char *pathname, const char *arg, ...);

// 示例:子进程执行"ls -l /home"
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
    pid_t child_pid = fork();

    if (child_pid == 0) {
        // 子进程:替换为ls命令
        printf("子进程执行ls -l\n");
        // 参数列表:程序名"ls",参数"-l",参数"/home",结束符NULL
        execl("/bin/ls", "ls", "-l", "/home", (char *)NULL);
        // 若execl返回,说明执行失败
        perror("execl执行失败");
        exit(1);
    } else if (child_pid > 0) {
        wait(NULL); // 父进程等待子进程完成
    } else {
        perror("fork失败");
        return -1;
    }

    return 0;
}

execvp () 函数原型与示例

#include <unistd.h>
/**
 * 以数组形式传递参数,自动搜索PATH(无需完整路径)
 * @param file 程序名(如"ls",自动搜索PATH)
 * @param argv 参数数组,格式:{程序名, 参数1, 参数2, ..., NULL}
 * @return 执行成功无返回;失败返回-1
 * @note 数组末尾必须是NULL,否则参数传递不完整
 */
int execvp(const char *file, char *const argv[]);

// 示例:子进程执行"ps aux"
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
    pid_t child_pid = fork();

    if (child_pid == 0) {
        char *const argv[] = {"ps", "aux", (char *)NULL}; // 参数数组
        execvp("ps", argv); // 自动搜索PATH中的ps程序
        perror("execvp执行失败");
        exit(1);
    } else if (child_pid > 0) {
        wait(NULL);
    } else {
        perror("fork失败");
        return -1;
    }

    return 0;
}

system () 函数(简化版 exec)

#include <stdlib.h>
/**
 * 执行shell命令(内部调用fork()+execl("/bin/sh", "sh", "-c", command, NULL))
 * @param command 待执行的shell命令字符串(如"ls -l")
 * @return 命令执行完成后的状态码;command为NULL时,返回是否有shell可用(非0表示可用)
 * @note 父进程会阻塞直到命令执行完成
 *       内部自动处理fork和exec,使用更简单,但效率略低
 *       避免传递用户输入的命令字符串(存在安全风险)
 */
int system(const char *command);

// 示例:执行"echo 'Hello Process'"
#include <stdio.h>
#include <stdlib.h>
int main() {
    printf("执行shell命令:\n");
    int status = system("echo 'Hello Process'");
    printf("命令执行完成,状态码:%d\n", status);
    return 0;
}

popen () 函数与pclose () 函数

popen ()

#include <stdio.h>
/**
 * 创建管道并执行shell命令,支持与命令的标准输入/输出进行数据交互
 * @param command 待执行的shell命令字符串(如"ls -l"、"grep 'test' file.txt",支持shell语法)
 * @param type 管道数据流向类型:
 *             - "r":读取命令的标准输出(命令→管道→当前进程)
 *             - "w":向命令的标准输入写数据(当前进程→管道→命令)
 * @return 成功:返回FILE类型指针(可通过fread/fwrite/fgets等文件流函数操作管道);
 *         失败:返回NULL,同时设置errno(如创建管道失败、fork子进程失败、shell执行失败)
 * @note   内部自动创建匿名管道+fork子进程,子进程通过"/bin/sh -c command"执行命令
 *         若type为"r",读取时若命令未输出会阻塞;若为"w",写入时若命令未读取会阻塞
 *         必须与pclose()配对使用,否则子进程会成为僵尸进程
 */
FILE *popen(const char *command, const char *type);

// 示例:子进程(popen内部自动创建)执行"ls -l /home",当前进程读取命令输出
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
    // 执行ls -l /home,以"r"模式读取命令输出
    FILE *fp = popen("ls -l /home", "r");
    if (fp == NULL) {
        perror("popen执行失败"); // 打印错误原因(如管道创建失败)
        return -1;
    }

    char buf[1024] = {0}; // 存储命令输出的缓冲区
    // 逐行读取命令输出(fgets遇换行或缓冲区满返回)
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        // 去除缓冲区末尾的换行符(可选,根据需求处理)
        buf[strcspn(buf, "\n")] = '\0';
        printf("命令输出:%s\n", buf);
    }

    // 关闭管道并回收子进程资源(必须调用pclose)
    int status = pclose(fp);
    if (status == -1) {
        perror("pclose执行失败");
        return -1;
    }
    printf("命令执行完毕,退出状态码:%d\n", status);

    return 0;
}

pclose ()

#include <stdio.h>
/**
 * 关闭popen()创建的管道,阻塞等待子进程执行完毕并回收资源
 * @param stream popen()返回的FILE类型指针(必须是popen创建的有效流,否则行为未定义)
 * @return 成功:返回命令的退出状态(需通过sys/wait.h的宏解析,如WIFEXITED/WEXITSTATUS);
 *         失败:返回-1,同时设置errno(如stream无效、等待子进程时被信号中断)
 * @note   会阻塞当前进程,直到popen创建的子进程完全退出
 *         若不调用pclose,子进程会残留为僵尸进程,占用系统PID资源
 *         退出状态解析逻辑与waitpid()一致,需结合宏判断命令退出原因
 */
int pclose(FILE *stream);

// 示例:向"cat"命令写数据,再通过pclose关闭管道并解析命令退出状态
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h> // 包含退出状态解析宏
int main() {
    // 执行cat命令(cat默认读取标准输入并输出),以"w"模式向命令写数据
    FILE *fp = popen("cat", "w");
    if (fp == NULL) {
        perror("popen执行失败");
        return -1;
    }

    // 向cat命令的标准输入写数据(fwrite返回实际写入字节数)
    const char *write_data = "Hello from popen()!\nThis is a test for pclose().";
    size_t write_len = fwrite(write_data, 1, strlen(write_data), fp);
    if (write_len != strlen(write_data)) {
        perror("向命令写数据失败");
        pclose(fp); // 即使写入失败,也要关闭管道避免僵尸进程
        return -1;
    }

    // 关闭管道并等待子进程退出,获取退出状态
    int status = pclose(fp);
    if (status == -1) {
        perror("pclose执行失败");
        return -1;
    }

    // 解析命令退出状态
    if (WIFEXITED(status)) {
        // 命令正常退出(如return、exit)
        printf("命令正常退出,退出码:%d\n", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        // 命令被信号终止(如kill -9)
        printf("命令被信号终止,信号编号:%d\n", WTERMSIG(status));
    } else {
        printf("命令退出状态未知\n");
    }

    return 0;
}

常用工具与命令

命令 / 工具 作用说明 示例
ps -aux 查看系统所有进程的详细信息(PID、状态、CPU 占用等) ps -aux | grep firefox(查找火狐进程)
pstree 以树状图显示进程间的父子关系 pstree -p(显示 PID)
objdump 查看 ELF 文件的节信息、反汇编代码 objdump -h demo.o(查看节信息)
file 查看文件类型(判断是否为 ELF 文件) file demo(判断 demo 是否为可执行文件)
kill 发送信号终止进程 kill -9 1234(强制终止 PID=1234 的进程)
gcc C/C++ 编译器,用于编译程序 gcc -g -o test test.c(生成带调试信息的可执行文件)
posted @ 2025-11-13 19:31  YouEmbedded  阅读(9)  评论(0)    收藏  举报