01进程

一、程序的概念与发展

(一)程序的定义

一般的讲,程序是一系列有序指令的集合,目的是告诉计算机如何完成某些指定的操作或者如何解决某个问题。

(二)编程语言的发展历程

早期:采用机器语言设计程序,开发效率极低。

中期:出现汇编语言,但不同平台的汇编指令集不兼容,增加了开发难度。

后期:为提升开发效率、降低难度,诞生高级语言。其中,C 语言属于面向过程编程,C++ 属于面向对象编程,此外还有 Python、Java 等多种编程语言。

(三)编程领域重要人物及贡献

img

(四)程序的文件格式

程序的格式和类型由所使用的编程语言决定,不同编程语言对应不同的源文件后缀名,例如:

  • C 语言:.c 后缀
  • Python 语言:.py 后缀

(五)程序的运行原理

程序无法直接运行,需经编译器等工具处理转换为可执行文件(内部包含对应平台可识别的指令和数据集合)。运行可执行文件的本质,是计算机中央处理器(CPU)对其中的指令进行处理、对数据进行运算的过程。

(六)计算机组成与程序执行流程

  1. 计算机组成:遵循冯・诺依曼结构,由控制器、运算器、存储器、输入设备、输出设备五部分组成。
  2. 程序执行依赖:需掌握计算机组成原理和汇编语言,才能理解程序中指令和数据的具体执行流程。

img

(七)程序运行的内存模型(32 位系统)

image-20260129183445720

二、程序的编译过程

(一)编译的必要性

编写完成的程序无法直接运行,需通过编译器编译生成可执行文件,嵌入式学习中常在 Linux 系统下使用 GCC 编译器。

img

(二)GCC 编译器简介

GCC编译器是Linux系统默认的C/C++编译器,大部分Linux发行版本中都是默认安装的。GCC编译器主要以Linux命令的形式在shell终端中使用,所以需要大家掌握关于GCC编译器的相关参数。

img

image-20260129183501265

(三)C 语言程序编译四步骤

C语言程序编译过程:源程序 ---- 预处理 --- 编译 --- 汇编 --- 链接 --- 可执行文件

预处理

对源码进行简单的加工,GCC编译器会调用预处理器cpp对程序进行预处理,其实就是解释源程序中所有的预处理指令,如#include(文件包含)、#define(宏定义)、#if(条件编译)等以#号开头的预处理语句。

这些预处理指令将会在预处理阶段被解释掉,如会把被包含的文件拷贝进来,覆盖掉原来的#include语句,把所有的宏定义展开,所有的条件编译语句被执行,GCC还会把所有的注释删掉,添加必要的调试信息。

预处理指令: gcc -E xxx.c -o xxx.i 会生成预处理文件 xxx.i

编译

就是对经过预处理之后的.i文件进行进一步翻译,也就是对语法、词法的分析,最终生成对应硬件平台的汇编文件,具体生成什么平台的汇编文件取决于编译器,比如X86平台使用gcc编译器,而ARM平台使用交叉编译工具arm-linux-gcc。

编译指令:gcc -S xxx.i -o xxx.s 会生成对应汇编文件 xxx.s

汇编

GCC编译器会调用汇编器as将汇编文件翻译成可重定位文件,其实就是把.s文件的汇编代码翻译为相应的指令。

编译指令:gcc -c xxx.s -o xxx.o 会生成汇编后的文件 xxx.o

链接

经过汇编步骤后生成的.o文件其实是ELF格式的可重定位文件,虽然已经生成了指令流,但是需要重定位函数地址等,所以需要链接系统提供的标准C库和其他的gcc基本库文件等,并且还要把其他的.o文件一起进行链接。默认链接 -lc(libc 标准库) -lgcc 是默认的,可以省略

编译指令:gcc hello.o -o hello -lc -lgcc 会得到可执行文件 xxx // l是lib的缩写

三、程序的格式与目标文件

(一)目标文件与可执行文件

目标文件(.o 文件):又称可重定位文件,是未完成链接的中间文件,可制作成 Linux 系统下的静态库(libxxx.a)或动态库(libxxx.so)。

可执行文件:完成链接后的文件,与目标文件同属 ELF(Executable Linkable Format,可执行可链接格式)。

查看文件格式:Linux 系统中使用file指令,例如file demo.o可查看目标文件属性,file demo可查看可执行文件属性。

注意:可以在linux系统中利用查看文件格式的指令: file 查阅目标文件和可执行文件的区别:

image-20260129183628676

image-20260129183619323

(二)目标文件的内容与结构

还未完成链接的目标文件中存储了哪些内容,以及如何查看目标文件中的内容信息???

目标文件中的内容至少有编译后的机器指令代码、数据。除了这些内容以外,目标文件中还包括了链接时所需要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”(Section)的形式存储,有时候也叫“段”(Segment),一般情况下,它们都表示一个一定长度的区域,基本上不加以区别。

程序源代码编译后的机器指令经常被放在代码段(Code Section)里,代码段常见的名字有“.code”或“.text”。全局变量和局部静态变量数据经常放在数据段(Data Section),数据段一般名字都叫“.data”。

注意:linux系统的GCC编译套件有一款工具叫做objdump,可以查看目标文件内部的数据

image-20260129183603887

数据段中有一个叫做.bss段, bss以前其实是汇编伪指令,作用是为某些数据预留一块空间,bss其实是Block Started by Symbol,也就是用于存储未被初始化的全局变量和静态局部变量,但是在程序编译阶段是没有分配空间的,在程序运行时会得到空间

image-20260129183530506

得到了运行空间

*常用的段名* *说* *明*
.rodatal 这种段里存放的是只读数据,比如字符串常量、全局const变量。跟".rodata"一样
.comment 存放的是编译器版本信息,比如字符串:“GCC:(GNU)4.2.0”
.debug 调试信息
.dynamic 动态链接信息
.hash 符号哈希表
.line 调试时的行号表,即源代码行号与编译后指令的对应表
.note 额外的编译器信息。比如程序的公司名、发布版本号等
.strtab String Table.字符串表,用于存储ELF文件中用到的各种字符串
.symtab Symbol Table.符号表
.shstrtab Section String Table.段名表
.plt 动态链接的跳转表和全局入口表
.init 程序初始化与终结代码段。

另外,objdump工具也可以实现把可执行文件进行反汇编,可以把得到的反汇编代码重定向到某个文本中进行查看 “objdump -D demo > > xxx.txt

四、进程的概念与特征

(一)进程的定义

当 ELF 可执行文件运行时,操作系统将文件中的指令和数据从外存加载到内存,为其分配内存空间、CPU 使用权等系统资源,程序从静态变为动态,这种正在执行的程序实例称为进程。进程是操作系统分配资源的基本单位!操作系统是以进程为单位来分配系统资源的,比如内存空间、CPU使用权等。 线程是操作系统调度资源的最小单位! 进程包含线程!

image-20260129183650285

(二)进程与程序的区别

特性 程序 进程
状态 静态(存储于磁盘) 动态(运行于内存)
资源占用 不占用系统资源 占用内存、CPU 等系统资源
存在形式 文本文件 包含 PCB、代码段、数据段的运行实体

(三)进程的四大特征

进程具有四个基本特征,分别是动态性、并发性、独立性、异步性,具体的区别如下所示:

  1. 动态性:进程是程序在处理器上的一次执行过程,会经历创建、暂停、继续执行、终止等状态变化。
  2. 并发性:多个进程可同时存在于内存中,并发执行以提高资源利用率。
  3. 独立性:进程是能独立运行的基本单位,也是系统资源分配和调度的独立单位。
  4. 异步性:进程以各自独立、不可预知的速度推进,执行进度受系统资源分配、其他进程抢占等因素影响。

五、进程的组成与管理

操作系统中可能会有n个进程同时执行,请问操作系统如何掌握这些进程的执行情况?

一般进程在创建之后,操作系统都会为进程分配一块内存来记录进程的各项参数信息,所以这块内存被称为进程控制块(Processing Control Block,缩写为PCB)。每个进程都有PCB,用于标识进程的存在以及记录进程的信息。

(一)进程的组成

进程由进程控制块(PCB)、代码段、数据段三部分组成:

  1. 进程控制块(PCB):操作系统为进程分配的内存区域,用于记录进程各项参数信息,标识进程存在,Linux 系统中以struct task_struct结构体存储(定义于sched.h头文件)。

    image-20260129183714446

  2. 代码段:可被调度程序调度到 CPU 执行的程序代码。

  3. 数据段:包含程序原始数据和执行过程中产生的中间数据。

(二)PCB 的核心内容

  1. 进程标识符

每个进程都有一个有系统分配的唯一的PID进程标识符(process identifier),用于区分系统中的其他进程,这个PID也是Linux内核提供给用户访问进程的一个接口,用户可以通过PID控制进程。

比如Windows系统可以通过任务管理器了解系统中正在执行的进程的数量以及进程的状态:

img

那Linux系统如何查看进程PID?

Linux系统中提供了关于获取进程状态的shell命令:ps ,该命令的使用方法详见man手册,一般使用 ps -ef 或者 ps -aux查看Linux系统中所有用户相关的进程的所有信息

image-20260129183737717

image-20260129183753049

  1. 进程当前状态

在进程的运行过程中由于系统中多个进程的并发运行和相互制约的结果,所以使进程的状态不断发生变化。操作系统以进程的状态来作为调度程序分配处理器的依据。

通常一个运行中的进程至少可划分为5种基本状态:就绪态、运行态、阻塞态、创建态、结束态

状态 说明
创建态 进程正在创建,系统分配 PCB 内存、填写管理信息、分配资源,未达到就绪态
就绪态 已获得除处理器外的所有资源,等待 CPU 调度,获得后可立即执行
执行态(运行态) 已获得所有资源(含 CPU),正在被 CPU 执行
阻塞态(暂停态) 执行过程中因某事件(如 sleep、read)无法继续,暂时停止
结束态 进程正常退出或异常中断,从系统中消失;若退出后资源未释放,进入僵尸态
僵尸态 进程终止但未释放资源,需用户处理,避免占用过多系统资源

其他信息:包括进程优先级、CPU 使用情况、内存占用情况、打开文件描述符等

image-20260129183813886

在Linux系统的终端中输入shell指令: ps -aux,就可以看到当前系统中运行的进程状态。

img

image-20260129183830743

标识 状态含义
D 不可中断睡眠(通常为 I/O 操作)
I 空闲内核线程
R 运行态或就绪态(在运行队列中)
S 可中断睡眠(等待事件完成)
T 被作业控制信号停止
t 调试过程中被调试器停止
W 分页(2.6.x 内核后无效)
X 死亡(极少出现)
Z 僵尸态(已终止但未被父进程回收)

注意:进程不是一直处于某一个状态,进程的状态会受到外界因素和执行进度的影响而发生改变,当然,进程的状态在某一个时刻是唯一的,也就是在某一时刻进程必须且只能处于一种状态。

六、进程的控制操作

进程控制指的是对系统中的所有进程实施有效的管理,其功能一般包括进程的创建、进程的撤销、进程的阻塞与唤醒等。这些功能一般是由操作系统的内核来实现的。

(一)进程的创建

进程关系

Linux系统中的一个进程中可以创建若干个新进程新创建的进程中又可以创建子进程,所以一般使用*进程前趋图来描述创建的进程之间的关系*。进程前趋图也被称为进程树,是为了描述进程家族关系的树。

img

比如在进程A中创建了一个新进程B,则进程B就是进程A的子进程,进程A就是进程B的父进程。

如果在进程A中创建了2个子进程B和C,进程B中创建了2个子进程D和E,进程C中创建了1个子进程F,则进程A就是该进程家族的祖先。

查看进程树

在Linux系统中运行的所有进程也可以构成一个进程树,并且该进程树也有一个祖先,用户可以通过Linux系统提供的shell命令:pstree 来打印进程关系

image-20260129183850190

所有进程的祖先为systemd进程(PID=1),即系统守护进程,负责系统启动时激活资源、管理其他进程。

守护进程也被翻译为精灵进程或者后台进程,是一种旨在运行于相对干净环境、不受终端影响的、常驻内存的进程,拥有不死的特性,长期稳定提供某种功能或服务。

systemd其实是一个 Linux 系统基础组件的集合,它提供了一个系统和服务管理器,然后运行为 PID 1的进程,负责在系统启动或运行时激活系统资源,并且管理服务器进程和其它进程。

img

创建接口

Linux系统中只有进程才可以在系统运行,所以一个程序想要运行,就必须为该程序创建进程。linux内核提供了一个名字叫做fork()的系统调用接口,该接口可以在进程中创建一个子进程。

image-20260129183911304

image-20260129183925684

fork()特性:

  • 子进程复制父进程的代码段、数据段、堆栈段,但拥有独立内存空间,父进程与子进程的内存操作互不影响。
  • 返回值:父进程中返回子进程 PID;子进程中返回 0;创建失败时父进程返回 - 1。
  • 执行顺序:父进程与子进程的执行顺序由操作系统调度器决定,可通过特定机制调整。

通过fork函数的返回值来区分父进程和子进程

获取当前进程PID的函数接口getpid(),获取当前进程的父进程PID的函数接口是getppid()

image-20260129184000216

练习

编写程序,要求在程序中创建一个子进程,让父进程和子进程分别打印不同的内容。


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

int main(int argc, const char *argv[]) 
{
    // 创建一个子进程
    pid_t pid = fork();
    // fork()函数返回值的判断
    if(pid>0){// 父进程
        printf("Parent process, pid: %d,My child pid: %d\n", getpid(), pid);
        while(1) ;
    } else if(pid==0){// 子进程
        printf("Child process, pid: %d,My parent pid: %d\n", getpid(), getppid());
    }else{
        perror("fork error");
        return -1;
    }
    return 0;
}

imgimage-20260129184040436

为什么会出现子进程的父进程的pid为1,因为当父进程的语句执行完后return结束了,就是子进程的父亲死了,所以由守护进程来看管刚才的子进程。

调用fork函数之后可以创建一个子进程,但是调用了一次fork函数为什么会有两个不同的返回值?

就是调用fork的时候子进程已经拷贝了父进程的代码段、数据段、堆栈段,所以fork函数还没有执行完成,就会在两个进程空间继续执行,就会得到两个不同的返回值。

(二)进程的撤销

一个进程在完成自身任务之后应该及时撤销,这样就可以及时的释放进程的资源,此时可以分为两种情况:一种是撤销指定进程,另一种是撤销指定进程以及该进程的所有子孙进程。不管是哪种情况,都应该及时回收进程占用的资源。

Linux系统中提供了一个名称叫做wait()的系统调用接口,该接口用于让父进程等待子进程的状态改变并获取已经改变状态的子进程的信息。

image-20260129184102205

僵尸态指的是进程终止但是并未释放相关资源的一种状态,此时由系统内核对处于僵尸态的进程进行维护,处于僵尸态的进程会占用创建进程的资源和数量,如果僵尸态进程数量太多,则会导致系统无法创建新进程。

所以父进程应该及时的对终止的子进程进行等待,这样就可以回收子进程占用的资源,当然,如果父进程终止,则处于僵尸态的子进程会由系统内核完成资源的回收

注意:如果当前进程没有子进程,则wait函数立即返回,如果当前进程有很多个子进程则wait函数会回收第一个变为僵尸态的子进程资源

wait函数的参数是一个指针,用于记录子进程的退出状态,如果该参数为NULL,则表示当前进程放弃子进程的退出状态。对于该指针中记录的值,用户可以通过系统提供的宏定义来分析子进程的退出状态。

image-20260129184120366

通过man手册可以发现Linux内核还提供了另一个叫做waitpid()的系统调用函数,该函数也可以等待子进程状态改变并回收子进程的系统资源,只不过该函数的针对性更强,可以指定回收某个子进程的系统资源

image-20260129184136297

练习:编程产生一个进程链,父进程派生一个子进程后,输出自己的PID,然后退出,该子进程继续派生子进程,然后打印PID,然后退出,以此类推,要求父进程比子进程先输出PID

int main(int argc, const char *argv[]) 
{
    int i=2;
    while( i-- ){
    // 创建一个子进程
    pid_t pid = fork();
    // fork()函数返回值的判断
    if(pid>0){// 父进程
        wait(NULL); // 等待子进程结束
        printf("Parent process, pid: %d,My child pid: %d\n", getpid(), pid);
    }else if(pid==0){// 子进程
        printf("Child process, pid: %d,My parent pid: %d\n", getpid(), getppid());
        break;
    }
    else{
        perror("fork error");
        return -1;
    }
    }  
    return 0;
}

Child process, pid: 3296,My parent pid: 3295
Parent process, pid: 3295,My child pid: 3296
Child process, pid: 3297,My parent pid: 3295
Parent process, pid: 3295,My child pid: 3297

要是子进程忘记加break,由于fork()时,会复制父进程的数据段和代码段

导致子进程中也会存在循环

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


int main(int argc, const char *argv[]) 
{
    int i=2;
    while( i-- ){
    // 创建一个子进程
    pid_t pid = fork();
    // fork()函数返回值的判断
    if(pid>0){// 父进程
        wait(NULL); // 等待子进程结束
        printf("Parent process, pid: %d,My child pid: %d\n", getpid(), pid);
    }else if(pid==0){// 子进程
        printf("Child process, pid: %d,My parent pid: %d\n", getpid(), getppid());
    }
    else{
        perror("fork error");
        return -1;
    }
    }  
    return 0;
}


Child process, pid: 3233,My parent pid: 3232
Child process, pid: 3234,My parent pid: 3233
Parent process, pid: 3233,My child pid: 3234
Parent process, pid: 3232,My child pid: 3233
Child process, pid: 3235,My parent pid: 3232
Parent process, pid: 3232,My child pid: 3235

image-20260129184153832

(三)进程的执行

既然在一个进程中可以创建多个子进程,并且子进程和父进程的数据段和代码段是相同的,也就是一份程序会执行两次,一般情况下是没有意义和必要的,请问能否让子进程单独的加载新的程序的代码段和数据段,如果可以,应该如何实现?

当然是可以的,Linux系统标准库中提供了一组函数接口来实现在进程中加载和执行指定的程序。

img

image-20260129184204869

这组接口可以把exec作为前缀,然后以exec后面的字符进行分类,比如l指的是list的缩写,p指的是path的缩写,e指的是environment的缩写,v指的是vector的缩写。

(1) l : 以列表的方式来组织指定程序的参数

(2) v: 以数组的方式来组织指定程序的参数

(3) e: 执行指定程序前顺便设置环境变量

(4) p: 执行程序时可自动搜索环境变量PATH的路径

image-20260129184218341

execl(const char pathname, const char arg, ...) 为例,参数pathname是需要加载的指定程序,而arg则是该程序运行时的命令行参数,命令行参数包括程序名本身,并且全部是字符串,其实和带参数的main函数通过命令行传参的流程类似,但是函数的参数列表必须以NULL结束**,也就是函数的最后一个参数填NULL即可。

练习:编写一个程序,使之产生一个子进程,在子进程中执行系统命令ls -l去查看某个文件的信息,父进程判断子进程是否执行成功。要求父进程等待子进程结束之后再结束。提示:Linux系统中的shell命令属于可执行文件

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

int main(int argc, const char *argv[]) 
{
    // 创建一个子进程
    pid_t pid = fork();
    // fork()函数返回值的判断
    if(pid>0){//   父进程
        printf("Parent process, pid: %d,My child pid: %d\n", getpid(), pid);
        wait(NULL);
        printf("child process end\n");
    } else if(pid==0){// 子进程
        printf("Child process start, pid: %d\n", getpid());
        //execl("/bin/ls", "ls", "-l", NULL);
        system("ls -l");
    }else{
        perror("fork error");
        return -1;
    }
    return 0;
}

其实,linux系统还提供一个函数,名称叫做system(),这个函数也可以让新进程执行shell命令。 image-20260129184238581

posted @ 2026-01-29 18:44  郭小胖  阅读(0)  评论(0)    收藏  举报