Linux_gdb调试器--进程概念
title: Linux_3 操作系统
认识冯诺依曼系统
硬件按照一定的规则组装形成计算机
冯诺依曼体系结构(Von Neumann Architecture)是现代计算机设计的理论基础,由美籍匈牙利数学家约翰·冯·诺依曼(John von Neumann)及其团队在1945年提出,并在报告《First Draft of a Report on the EDVAC》中首次系统阐述。这一结构至今仍是绝大多数计算机的核心设计框架。
冯诺依曼体系结构将计算机分为五大基本部件:运算器(ALU, Arithmetic Logic Unit)负责所有算术运算(加减乘除)和逻辑运算(与或非)。控制器(Control Unit)从内存读取指令,解码并协调其他部件工作(如控制数据流向)。存储器(Memory)存储程序指令和数据(现代计算机分为高速缓存、主存、外存等层次)。输入设备(Input Devices)将外部数据或指令输入计算机(如键盘、鼠标)。输出设备(Output Devices)将处理结果输出(如显示器、打印机)。
数据是在计算机体系结构中进行流动的, 从一个设备到另一个设备, 本质是一种拷贝
数据设备的拷贝效率决定了整机的基本效率
存储器用来弥补设备间的效率差距
操作系统
是一款软硬件资源管理的软件, 核心六个字: 先描述, 再组织
操作系统的内核 + 操作系统的外壳程序(给用户提供操作)
操作系统 : 进程管理, 内存管理, 文件管理, 驱动管理。
操作系统与硬件层存在驱动层, 方便操作系统管理
用户直接使用操作系统是复杂的并且不安全的, 所以操作系统提供了系统调用接口(system call)
用户直接调用系统接口,有一些难度,所以用户通过用户操作接口操作(shell)
向上提供稳定 高效 安全 的运行环境
进程
操作系统中同时存在非常多的进程, 操作系统依旧通过先描述再组织的方式管理进程
基本概念:进程是正在执行的程序实例,是操作系统进行资源分配和调度的基本单位。它不仅包含程序的代码(文本段),还包括当前的活动状态(如程序计数器、寄存器值)、堆栈(临时数据)、数据段(全局变量)以及占用的系统资源(如打开的文件、内存等)。
内核观念: 担当分配系统资源(cpu时间, 内存)的实体
一个可执行程序, 是在磁盘中的文件, 执行这个程序,需要把这个二进制程序加载到内存中, 存在一个结构体(PCB) 包含进程的所有属性 (id, 优先级…)
进程的组成
- PCB(Process Control Block):操作系统为每个进程维护的数据结构,包含:
- 进程ID(PID):唯一标识符。
- 进程状态:如就绪、运行、阻塞等。
- 程序计数器:指向下一条要执行的指令。
- 寄存器值:CPU上下文(如累加器、栈指针)。
- 内存管理信息:页表、内存限制等。
- I/O状态:分配的设备、打开的文件列表。
- 代码段:程序的指令(只读)。
- 数据段:全局变量、静态变量。
- 堆栈段:局部变量、函数调用信息。
理解进程
while :; do ps ajx | head -1 && ps ajx | grep myprocess; sleep 1; done # 查看进程
把对进程的管理转换为对链表(PCB组成的)的管理
进程是 tast_struct + 程序
linux 下绝大多数指令本质都是运行进程
ps ajx | head -1 && ps ajx | grep [可执行程序] # 查看进程信息和状态
进程关键属性 PID: 每一个进程的唯一标识符
用户不能直接访问进程获得进程PID, 系统提供了调用接口 pid_t geipid()
gitppid() 获得进程的父进程
ctrl + c / kill -9 [pid] 结束进程
一个进程每次启动pid 不同, ppid是相同的
pid_t fork() 创建子进程
forK() 之后父子进程代码共享
观察运行结果
#include <stdio.h>
#include <unistd.h>int main() {printf("process is running... pid : %d\n", getpid());sleep(3);fork();printf("hello world, pdd : %d, ppid : %d\n", getpid(), getppid());sleep(4);
}
fork 返回两次(fork 函数内部子进程已经创建), 返回0表示子进程, 返回-1表示进程创建失败
#include <stdio.h>
#include <unistd.h>int main() {printf("process is running... pid : %d\n", getpid());sleep(3);pid_t id = fork();if(id == -1) {return 1;} else if(id == 0){while(1) {printf("hello i am child process, pdd : %d, ppid : %d\n", getpid(), getppid());sleep(1);}} else{while(1) {printf("hello i am parents process, pdd : %d, ppid : %d\n", getpid(), getppid());sleep(1);}}
}
进程一定要做到, 进程具有独立性
同时创建多个进程 代码示例
#include <stdio.h>
#include <unistd.h>void RunChild() {while(1) {printf("I am child, pdi %d, ppid %d\n", getpid(), getppid());sleep(1);}
}int main() {const int num = 5;for (int i = 0; i < num; i++) {pid_t id = fork();if(id == 0) {RunChild();}}while(1) {printf("I am parent, pdi %d, ppid %d\n", getpid(), getppid());sleep(1);}
}
pcb中存在一个cwb(current work dir), 每个进程启动的时候, 会记录自己当前那个路径启动
进程状态
linux 进程状态
tast_struct 中的一个标志位, 表示状态, int status
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main() {while(1) {printf("run..,\n");sleep(1);}return 0;
}
S: 上述代码运行, 大部分状态是S(等待),cpu的速度远大于显示器调度的速度, S是休眠状态。
T 状态,让进程暂停, kill -19 [进程id] 暂停进程, kill -18 [进程id] 开始进程,
代码调试本质就是进程暂停
D disk sleep 状态, linux操作系统有权杀掉进程,释放空间, D状态表示该进程不可被杀掉(一i些重要的不可被中断的进程)
状态 Z, 僵尸进程,已经运行完毕, 但是需要维持自己的退出信息, 未来让自己的父进程进行读取。如果没有父进程读取, 僵尸进程会一直存在
孤儿进程: 父进程比子进程先退出
下面代码观察僵尸进程和孤儿进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main() {pid_t id = fork();if(id == 0) {// childint cnt = 0;while(1) {printf("i am child, cnt : %d\n", cnt);sleep(1);cnt++;}} else{// parentsint cnt = 5;while(cnt) {printf("i am parent, running ... pid : %d\n", getpid());sleep(1);cnt--;}}// while(1) {// printf("run..,\n");// sleep(1);// }return 0;
}
命令行中的进程,父进程是bash,会自动回收, 不会出现僵尸进程
进程在运行队列中, 该进程的状态是就绪状态。cpu有自己的运行队列
一个进程一但持有了cpu, 并不会一直运行该进程。
进程间是基于时间片轮番调度的,多个进程以切换的方式进行, 这就是并发。linux不是这么调用的
阻塞状态
在计算机科学和操作系统领域,阻塞状态(Blocked State)是指一个进程或线程因为等待某些资源或事件而暂时无法继续执行的状态。
定义
当一个进程/线程因某些条件未满足(如等待I/O操作完成、获取锁、收到信号等)时,操作系统会将其移出就绪队列,使其进入阻塞状态。此时,它不会占用CPU资源,直到所需条件满足。
挂起状态(Suspended State)
是指进程被暂时移出内存(换出到外存,如磁盘),并处于一种静止状态,直到被重新激活。挂起通常由操作系统或用户主动触发,目的是释放内存资源或控制进程的执行。
进程在切换, 最重要的是上下文数据的保存和恢复
进程优先级
在 Linux 系统中,进程优先级决定了进程获取 CPU 资源的顺序。优先级高的进程会优先获得 CPU 时间。Linux 使用两种不同的优先级机制:静态优先级(Nice 值)和动态优先级(实时优先级)。
操作系统中优先级尽量不对进程优先级进行改动。这块不是很重要
环境变量
命令行参数
main函数带参, int main(int argc, char *argv[]) {}
这些参数可带可不带
在C和C++程序中,main
函数可以带有参数,这允许程序从命令行接收输入。这种带参数的main
函数形式通常用于处理命令行参数。
argv[0] 表示程序名称
命令行输入 ./myprocess 1 观察结果·
#include <stdio.h>
#include <unistd.h>
#include <string.h>int main(int argc, char* argv[]) {printf("%s\n", argv[0]);if(strcmp(argv[1] , "1") == 0) {printf("hello 1\n");}else if(strcmp(argv[1] , "2") == 0) {printf("hello 2\n");}return 0;
}
命令行参数本质是交给程序不同的选项, 执行不同的程序功能
命令行参数是父进程bash传递的
环境变量
执行命令和执行程序是一个意思, 但是执行我们所i写程序必须指明程序路径。“ls”, “pwd” 这些命令就不需要。
主要原因是linux系统中存在一些全局变量,告诉命令解释器, 应该去那些路径下寻找可执行程序
系统中很多的配置, 登录时已经加载到了bash进程中。
PATH, 打印环境变量 $PATH。
echo $PATH # 显示环境变量
结果 : /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
bash在执行命令时, 需要先找到命令, 然后把二进制程序加载到进程。bash就是在PATH中找。
ls 所在的命令在 /usr/bin中, 所以不需要路径
可以选择把自己的某个目录加到环境变量中,
PATH=&PATH:[路径] # 但是这只是内存级的, 重新登录就会回复
最开始的环境变量不是在内存中的, 而是在系统的配置文件中。
执行内建命令不需要创建子进程
程序的地址空间
观察代码结果
#include <stdio.h>
#include <unistd.h>
#include <string.h>int g_val = 100000;int main(int argc, char* argv[]) {printf("I am father process, pid: %d, g_val: %d\n", getpid(), g_val);sleep(3);pid_t id = fork();if (id == 0) {// childwhile(1) {printf("I am child process, pid: %d, g_val: %d, address: %p\n", getpid(), g_val, &g_val);g_val--;sleep(1);}}else {// fatherwhile(1) {printf("I am father process, pid: %d, g_val: %d, address: %p\n", getpid(), g_val, &g_val);sleep(1);} }return 0;
}
g_val 同时出现了两个值。这是满足进程间数据的独立性。
他们的地址却是相同的:这个地址不是真正的地址, 而是虚拟地址
虚拟地址通过页表对应物理地址, 当子进程修改数据时, 会重新分配物理地址(操作系统自主进行写时拷贝)
每个进程都会分配一个地址空间
地址空间的本质就是内核中的一个结构体对象
地址空间的意义: 将无序变为有序,让进程以统一的视角看待物理内存以及自己运行的各个区域
将进程管理模块和内存管理进行解耦
地址空间的设计从根本上解决了以下几个核心问题:
- 如何让多个程序安全地共享物理内存 → 通过虚拟化提供隔离
- 如何让程序不受物理内存限制 → 通过分页/交换机制扩展
- 如何简化程序的内存访问 → 提供统一的线性地址视图
- 如何保护系统核心不受破坏 → 划分用户/内核空间
写时复制机制:
- 当调用 fork() 时,操作系统并不会立即复制父进程的所有内存给子进程
- 相反,父进程和子进程最初共享相同的物理内存页
- 只有当任一进程尝试修改这些共享页时,操作系统才会为该进程创建该页的副本
虚拟内存地址:
- 虽然父进程和子进程中打印的 g_val 地址(虚拟地址)相同,但这只是虚拟地址空间的相同
- 在物理内存中,一旦发生写操作(如子进程中的 g_val–),这两个变量实际上位于不同的物理页
进程控制
fork() 函数,创建子进程
进程 = 内核的相关管理数据结构 + 代码和数据
创建子进程:
分配新的内存块和内核数据结构给子进程
将父进程的部分数据结构内容拷贝给子进程
为了方便父进程对子进程进行表示, fork() 函数 父进程是返回子进程id,子进程返回0
进程终止
释放代码和数据所占用的空间
释放内核数据结构
echo $? # 父进程bash获取到的最近的一个进程的退出码
退出码非0表示失败, 数字不同表示失败的原因
打印错误码对应信息
for (int errcode = 0; errcode <= 255; errcode++) {printf("%d, %s\n", errcode, strerror(errcode));
}
父进程bash需要知道子进程退出码, 得知成功/错误原因, 显示给用户
main函数的return就是进程退出码
进程终止的三种情况
代码跑完,结果正确
代码跑完,结果不正确 (通过进程退出码确定)
代码执行时出现了异常,提前退出
先确认是否异常,再看退出码
衡量进程退出需要看两个数字,退出信号, 退出码
exit 和 _exit 都表示在任意位置结束进程。他们的区别是_exit 不会刷新缓冲区,
exit 是C库函数的调用方法,_exit是系统调用。可以得出缓冲区不在操作系统中。
进程等待
子进程退出, 父进程不管不顾可能造成僵尸进程的问题, 进而造成内存泄漏
父进程需要子进程的结果, 父进程通过等待的方式回收子进程资源。获取子进程信息
pid_t wait(int * status); #父进程等待任意一个子进程退出,等待成功时, 返回子进程pid
如果没有子进程退出, 父进程一直在进行阻塞等待
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>void ChildRun() {int cnt = 5;while (cnt) {printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);sleep(1);cnt--;}
}int main() {printf("I am father, pid: %d, ppid: %d\n", getpid(), getppid());pid_t id = fork();if(id == 0) {ChildRun();printf("child quit...\n");exit(0);}// fathersleep(10);pid_t rid = wait(NULL);if(rid > 0) {printf("%d\n", rid);}
}
pid_t waitpid(pid_t pid, int& status, int option)哪 个进程 输出型参数-1 表示任意子进程 退出信息通过二进制分割,分别表示退出信号和状态
这个 status
是一个整型值(int
),其内部由多个字段组成,记录了子进程的退出方式(正常退出、信号终止、暂停等)以及附加信息(如退出码或信号编号)
| 31…16 (保留) | 15…8 (退出状态或信号) | 7 (core dump 标志) | 6…0 (终止信号) |
optiona : WNOHANG非阻塞等待,非阻塞轮询, 允许进程做一些其他的事情
非阻塞等待, 父进程dootherthing, 代码示例
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "task.h"void ChildRun() {int cnt = 5;while(cnt) {printf("I am child process pdi %d, ppid %d, cnt : %d\n", getpid(), getppid(), cnt);sleep(1);cnt--;}
}typedef void(*func_t)();func_t tasks[3] = {NULL};void LoadTask() {tasks[0] = PrintLog;tasks[1] = DownLoad;tasks[2] = MysqlDate;
}void HanderTask() {for(int i = 0; i < 3; i++) {tasks[i]();}
}void DootherThing() {HanderTask();
}int main() {printf("I am parent process, pid %d, ppid %d\n", getpid(), getppid());pid_t id = fork();if(id == 0) {ChildRun();printf("child quit...\n");exit(123);}// 父进程非阻塞等待LoadTask();int status;while(1) {pid_t rid = waitpid(id, &status, WNOHANG);if(rid == 0) {usleep(100000);printf("child is runing, father check next time\n");DootherThing();}else if(rid > 0) {if(WIFEXITED(status)) {printf("quit sucess, exit code: %d\n", WEXITSTATUS(status));}else{printf("quit unnormal\n");}break;} else{printf("waitpid failed\n");break;}}return 0;
}
进程替换
exec* 函数, 可以执行新的程序
exec* 函数,执行完毕后,后续代码不见了, 被替换了( 夺舍了(bushi
int main() {printf("run...\n");execl("/usr/bin/ls", "ls", "-l", "-a", NULL);printf("end...\n");return 0;
}
进程间具有独立性, 所以子进程被替换不影响父进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>int main() {printf("run...\n");pid_t id = fork();if(id == 0) {//childexecl("/usr/bin/ls", "ls", "-l", "-a", NULL);}int status = 0;pid_t rid = waitpid(id, status, 0);printf("end...\n");return 0;
}
execl (cosnt char* path, const char* argv, …);
参数表示程序路径, 命令行输入的内容
execv(const char* path, const char* argv[])
execvp(const char* file, char *const argv[])
替换例子:
int main() {printf("run...\n");pid_t id = fork();if(id == 0) {//childexecl("/usr/bin/python3", "python3", "a.py", NULL);}int status = 0;pid_t rid = waitpid(id, status, 0);printf("end...\n");return 0;
}