文章目录
【Linux】深入理解Linux的进程(二)
【Linux】深入理解Linux的进程(一)-优快云博客讲解了冯诺依曼结构、进程状态、孤儿进程、子进程、
tash_struct的部分属性。本文讲解Linux进程调度的真实情况、环境变量、命令行参数、页表、程序地址空间、进程控制、status
进程优先级
进程优先级决定了进程得到CPU资源的先后顺序,可以使用
ps -al查看进程优先级,其中-l表示使用长格式显示详细信息,-a表示显示所有用户的进程,上篇文章使用到了-axj,-x选项可以显示除当前会话外的其他进程,-j选项表示以作业形式显示详细信息,另外,使用-n可以把user和group以id形式打印。
- 进程优先级PRI=PRI默认值+NI(修正值)。PRI默认值等于80,NI的范围在
[-20, 19],所以PRI的范围在[60, 99] - 操作系统分为分时操作系统和实时操作系统,分时操作系统基于时间片公平调度,实时操作系统可以直接进行优先级抢占,让优先级高的进程抢占优先级低的进程工作被广泛应用于工业、制造业等领域。现在的分时操作系统也引入了抢占机制,不对分时操作系统的抢占机制进行讲解。
- slab:在进行进程调度本质就是对
task_struct进行操作,将正在运行的进程的task_struct放入runqueue就是运行,把阻塞某个设备的进程的task_struct放入struct device{task_struct* sleepqueue; // ..};就是阻塞。运行完的进程的task_struct空间不会被释放,而是会进入slab的``unuselist(数据结构对象的缓存)中,下次使用task_struct直接向slab`申请然后初始化,而不是向内存申请,减少申请时间。 - 使用指令修改进程优先级:
top显示不同进资源占用情况,然后在此状态使用r开始指定pid,再输入想要的NI值按回车即可- 进程优先级一般由OS决定,自己不进行修改,自己修改可能导致进程饥饿(一个进程长时间占用CPU资源)、也可能因为优先级倒置导致死锁等问题,因此不再讲解相关的系统调用和其他修改
NI值的指令。
cat /proc/cpuinfo可以查看cpu使用情况
进程切换
进程时间片耗尽等导致进程终止,或者阻塞IO等让进程进入sleep阻塞态,都会切换新进程到CPU运行。从内核态返回用户态时,OS会进行检查,选择新的进程运行。
进程切换分两种操作:
-
记忆旧进程状态——先将寄存器中的内容存放在内存的一个区域中:CPU中集成了寄存器,进程在使用CPU时,将页表地址(存储在CR3 register)、部分数据、当前执行位置等信息存储在寄存器中。切换进程后,CPU资源被新进程使用,为了防止旧进程存储在寄存器的资源丢失、方便保护和恢复旧进程的硬件上下文数据,所以先将寄存器中的内容存放在内存的一个区域中。
- 之前将这部分数据存储在
task_struct的tss_struct中,不同Linux版本,位置有变化
- 之前将这部分数据存储在
-
切换新进程——将
task_struct放入CPU,将之前存储在寄存器的信息再存入寄存器中,读取寄存器存储的数据,例如程序计数器(PC)、栈指针(SP)、段寄存器(CS-代码段、SS-栈段)、标志寄存器等等中的数据(当然,OS会设置内核数据结构管理寄存器)。
Linux真实调度算法
本文讲解Linux真实调度算法——O(1)算法,不过要注意的是现在Linux新内核都是用CFS完全公平调度算法。


环境变量
我们在配置开发环境的时候,经常会在Windows下配置环境变量PATH,因为系统会在启动对应程序时默认从PATH中按顺序遍历路径,可以通过本文深入理解、修改环境变量。
命令行参数
使用Java经常会写到命令行参数,平常写的C/C++不常用,这里解释什么是命令行参数,怎么用、有什么用:
-
int main(int argc, char* argv[]),这个()里的内容就是命令行参数 -
使用命令行输入
./code -a b c,在命令行输入这一串内容后,由bash进行命令解析,./code -a b c拆分成四个char*类型,赋值到argv[]中,并且记录个数argc为4。./code是运行当前路径下code可执行文件,可以在main函数中对argv操作,读取./code.-a.b.c完成个性化操作。-
代码示例:
int main(int argc, char* argv[]) { if(strcmp(argv[1], "-a") == 0) { //TODO } return 0; }
-
-
实际上,main最多有3个参数
main(int argc, char* argv[], char* env[])其中env由父进程传递,对于再命令行上写的进程,他们的父进程都是bash,他们如果再去创建进程,就会将env接着传递给孙进程,所以环境变量有全局特性。而main并不是系统最开始运行的函数,Linux入口函数是_start,Windows入口函数是crt_start,先设置ret,用于接受main的返回值,还有int arg_count;记录参数个数等等。最后根据ret处理main的退出信息。 -
可以通过main的参数传递给子进程自己的条件变量,但是不能修改父进程条件变量
环境变量
指令:
-
查看所有环境变量——
env -
查看指定环境变量(以PATH为例)内容——
echo $PATH -
修改特定名称环境变量内容——
env 名称=内容 -
如何向PATH添加新路径——
env PATH=$PATH:新路径 -
添加环境变量——
export MTENV=11223344 -
删除环境变量——
unset MTENV- 可以通过main的参数传递给子进程自己的条件变量,但是不能修改父进程条件变量。这里为什么可以对全局环境变量添加和修改了?
- 因为这个
export和unset是内建命令,由bash自己调用函数、系统调用亲自执行
-
i = 10,bash会记录两套变量,一套环境变量,一套本地变量,再命令行中写i=10,echo $i可以得到10。本地变量不会被子进程继承 -
使用
logout会退出当前用户,set可以查看全部变量
如何从存储角度理解环境变量?环境变量最开始从哪来?
登录时,
bash进程从system中/etc/bashrc读取环境变量配置文件,再根据~/.bashrc和~/.profile本机环境变量配置文件,创建环境变量表(本质是char* []),将环境变量写入。命令行的命令由bash拿到,构建命令行参数表解析命令,然后拿着命令在环境变量表中找到PATH,由bash寻找命令在system中是否存在,如果存在,创建子进程执行。
认识更多的环境变量

系统调用:
getenv("USER"):获取PATH的环境变量,返回char*,此时可以用char* who接受返回值,与lpx进行strcmp,判断当前用户是谁,可以根据不同用户展现个性化内容。setenv()putenv()- 全局变量的使用:只要
extern char** environ进行声明就可以使用全局的环境变量了。
可能产生的疑问
使用putenv,setenv会对全局/儿子环境变量有影响吗? execvpe的环境变量缺省值传的是全局environ还是此进程的,如果我函数内部声明extern char* environ,再使用char**手段修改environ,会对全局环境变量有影响吗*
核心理解:环境变量的"全局"是相对于进程而言的,不是系统全局!
所进行的一切修改都是在对此进程而言的,只会影响此进程和后续儿子进程的环境变量,不影响全局!
使用缺省值传参,传的是此进程的环境变量
不会有影响!!环境变量的"全局"是相对于进程而言的,不是系统全局!
程序地址空间
使用代码:
#include <stdio.h>
#include <unistd.h>
int gval = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("子: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());
sleep(1);
gval++;
}
}
else
{
while(1)
{
printf("父: gval: %d, &gval: %p, pid: %d, ppid: %d\n", gval, &gval, getpid(), getppid());
sleep(1);
}
}
}
我们创建子进程,父子进程共享代码和数据,在堆数据进行修改时,会进行写时拷贝,我们使用子进程修改数据gval,然后堆gval数据和地址进行打印,发现打印的地址一样,但是数据不一样。

-
这是因为OS在物理空间上封装了虚拟地址空间,这么做可以保障安全,不让用户访问到内存中的危险数据。这个虚拟空间上的地址被称为虚拟地址,物理内存上的地址被称为物理地址。
-
由页表完成虚拟地址和物理地址之间的映射,页表具体内容在页表专门讲解。
-
另一方面可以让进程以为自己得到了十分大的空间,在堆程序进行编码的时候,系统会根据不同类型数据形成不同segment,最后带着可执行文件的不同信息,一起组成ELF文件,形成ELF文件后,程序的逻辑地址就已经确定了、虚拟地址也确定了。举个场景:
-
你的代码需要4G内存空间才能运行,但是你的物理内存只有2G,在32位系统下,创建4G虚拟地址空间,然后将2G的代码加载到物理内存中,由页表建立映射关系,当访问到其他2G空间的时候,系统发生缺页中断,将访问的新的数据加载到内存中,将之前已经加载到内存的数据覆盖掉,需要时再进行加载即可。而在64位架构下,虚拟地址空间很大很大,最大可以开到(2^64 - 1),完全大于硬件的物理内存,这一操作让进程认为自己独占所有的物理空间。
虚拟地址空间创建的意义
-
内存保护与安全性
-
进程隔离:每个进程拥有独立的虚拟地址空间,确保进程之间的内存完全隔离
-
防止恶意访问:一个进程无法直接访问或修改另一个进程的内存区域
-
系统稳定性:防止程序错误导致整个系统崩溃,提高系统的健壮性
-
权限控制:可以为不同的内存区域设置不同的访问权限(读、写、执行)
-
-
内存管理的简化与优化
- 统一地址视图:为每个进程提供连续、统一的地址空间,简化程序设计
- 物理内存碎片化解决:虚拟地址的连续性掩盖了物理内存的碎片化问题
- 灵活的内存分配:操作系统可以更灵活地分配和回收内存资源
-
虚拟内存技术支持
-
扩展可用内存:程序可以使用比实际物理内存更大的地址空间
-
分页机制:支持将不常用的内存页面交换到磁盘,释放物理内存
-
按需加载:只有在实际访问时才加载内存页面,提高内存利用效率
-
统一编程接口:所有进程都使用相同的地址空间模型进行编程
-
降低开发复杂度:简化了内存管理相关的编程工作
-
提高代码可移植性:程序在不同硬件平台上具有更好的兼容性
-
-
共享区作用:
- 内存映射文件:支持将文件映射到虚拟地址空间,实现高效的文件访问
- 共享内存区域:允许不同进程共享特定的内存区域,实现进程间通信
- 动态链接库:支持多个进程共享同一份代码库,节省内存资源
- 为多线程程序提供更好的内存访问性能:在共享区创建栈空间给新的轻量化进程
-
性能优化
- 局部性原理利用:配合CPU缓存机制,提高内存访问效率
- 预取机制:支持内存预取策略,减少内存访问延迟
-
虚拟地址空间共享区可以完成过进程共享物理内存的作用,这也是进程间通信shm创建共享空间的原理

怎么管理虚拟地址空间
虚拟地址空间先使用结构体进行描述,对开辟、释放空间的一系列操作都对描述字段维护,以此来方便管理。所以先讲解怎么对虚拟地址空间进行描述。对于管理,使用链表数据结构进行管理vm_area_struct,如果vm_area_struct太多,就使用红黑树进行管理。
虚拟地址空间是进程独占的,每个进程都有自己的虚拟地址空间,OS将虚拟地址空间描述为struct mm_struct,并作为成员放在struct task_struct中,每个mm_struct都有vm_area_struct完成不同区的映射

-
vm_area_struct(简称VMA)包含以下关键字段:- vm_start:虚拟内存区域的起始地址
- vm_end:虚拟内存区域的结束地址
- vm_prot:内存保护标志(读/写/执行权限)
- vm_flags:各种标志位
- vm_next:指向下一个 VMA 的指针(形成链表)
-
我们使用mmap创建空间会创建新的
vm_area_struct,注意的是,这里是mmap系统调用,而不是malloc,mallc会根据申请空间的大小调用不同系统调用,空间大于一个阈值后调用mmap,小于的时候调用brk,使用brk申请小空间通常不会创建新的vm_area_struct,而是在原有的vm_area_struct上移动vm_end// 每次调用都会创建新的 VMA void *ptr1 = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); void *ptr2 = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); // 这会创建两个独立的 VMA
部分源码:
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* last find_vma result */
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
unsigned long mmap_base; /* base of mmap area */
unsigned long task_size; /* size of task vm space */
unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */
unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
int map_count; /* number of VMAs */
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* Protects page tables and some counters */
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
/* Special counters, in some configurations protected by the
* page_table_lock, in other configurations by being atomic.
*/
mm_counter_t _file_rss;
mm_counter_t _anon_rss;
unsigned long hiwater_rss; /* High-watermark of RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
unsigned long total_vm, locked_vm, shared_vm, exec_vm;
unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
};
以下字段划分形成不同区域,比如start_code brk就是完成堆地址的区域划分。
unsigned long total_vm, locked_vm, shared_vm, exec_vm;
unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
页表
- 页表除了完成虚拟地址向物理地址的映射,还设置权限位、是否要进入内核态、是否可命中等信息。
- 页表将无序的地址变成“有序”的地址,利用局部性原理提高效率,
- 同时还方便进行内存管理、减少内存碎片。
- 在进行地址变换中,对地址和操作验证合法性,从而保护物理内存。还可以将进程管理和内存管理解耦,只需要访问虚拟地址空间即可,内存管理由OS完成。
- 比如,要访问[3,4]G内核空间数据,就必须进入内核态才能访问
- 比如,你要对特定地址解引用,如果页表没有对应地址映射关系,就会发生段错误。(关于段错误原理,在信号一章节部分讲解)

一些问题:
-
页表在哪?
- 页表分为两种:内核页表和用户页表。分别完成内核空间和用户空间的映射,两者都在内核空间里
-
可不可以不加载代码和数据,只有
task_struct、mm_struct、页表等?- 可以
-
创建新进程,先加载代码和数据,还是先有
task_struct、mm_struct、页表?- 后者
-
如何理解进程挂起
- 查页表,把页表映射到的物理地址处的数据放入磁盘swap分区中。
-
free地址后,页表有什么变化
- 把对应虚拟地址映射关系删除,如果再访问这一虚拟地址,查页表查不到,就会段错误
-
为什么
char* str = "abcd"; *str='H';会崩溃。- 这是因为“abcd”被硬编码代码段,页表会对代码段区域设置权限——不可以修改,那么你的修改操作就会崩溃。
-
缺页中断是什么
- 在将磁盘中内容向内存加载的过程中,采取分批加载的策略,一开始只加载一部分代码和数据,但是会i根据ELF信息完成页表的建立(页表对未加载代码和数据的部分添加“不可命中”标识)。当进程在运行中访问到特定地址,发现“不可命中”,说明这部分数据不再内存上,将磁盘ELF文件再次向内存加载,这一过程就是缺页中断。
- 缺页中断证明进程管理和内存IO解耦
-
ps: 在创建子进程的过程中,会申请新的内存块和内核数据结构(页表、mm_struct、vm_area_struct),然后cp task_struct内容到子进程task_struct中,然后进入runqueue开始调度
进程控制
写时拷贝
原理:
- 在创建子进程后,系统会通过页表将数据段的写权限删除;
- 此时如果子/父进程尝试修改数据,就会触发页面错误(Page Fault)
- 内核捕获异常 → 检查是写时拷贝(COW)的页面
- 内核分配新页面 → 复制原页面内容
- 更新页表映射 → 指向新的物理页面,恢复写权限 → 继续执行写操作
意义:减少创建进程的时间,减少内存浪费
退出码
基础知识
获取退出码:使用echo $?指令获取进程退出码,对于父子进程,父进程可以通过进程等待获取子进程退出码。此外,C标准库系统调用函数执行错误会设置errno,可以使用perror、error打印错误消息,或者return errno;让errno以退出码形式返回,要注意的是:不要混淆errno和退出码,这两个没有直接关系,退出码是程序员设置的,要想让退出码等于errno,要程序员手动return errno
进程退出的情况:
- 自然退出,运行结果正确:exit(0)或者 return 0退出
- 运行结果错误:exit(!=0)或return 非0
- 异常错误终止运行:如果你没有对信号进行捕捉处理,导致进程终止,此时退出码无意义。因为你不知道进程运行到哪里出问题了,是否完成了相应的任务。
系统调用
_exit
exit()不是系统调用,他是C封装的函数;_exit()是系统调用,前者封装了后者。
此外,使用前者会刷新内核缓冲区到目标文件,使用C/C++标准完成析构等后续操作,最后调用_exit退出;
使用_exit()会直接退出,有以下不同:
-
速度会更快
-
不会刷新内核缓冲区
-
不关闭文件流:由内核强制回收文件描述符,
-
不删除临时文件:临时文件可能残留
-
直接系统调用:立即调用内核终止进程
-
有关资源回收的文件描述符表、虚拟内存空间、信号处理表、进程控制块(task_struct)都会交给内核回收。
wait waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);// options传 WNOHANG 表示不阻塞等待,传入0表示阻塞等待
// waitpid返回值 >0 子进程退出
// <0 wait defalut
// ==0 进程未退出
// pid分为四种情况
// >0 指定pid
// =0等待同GID的所有子进程
// =-1 等待所有子进程,等同于wait函数等待方法
// <-1 等待对应的GID的子进程(相反数)
这个status要注意和退出码区分

提供了宏来获取退出码、判断是否正常退出:
WEXITSTATUS(int status)返回退出码,即(status >> 8) & oxFFWIFEXITED(int status)返回 bool 异常返回false
程序替换
函数:exec*—— (其中execve是系统调用,其他的都是C语言封装的)
execl():l表示可变参数list传参,路径+文件名表示执行谁,传参末尾以NULL结尾表示参数传递完毕execlp()这个p表示只需要告诉文件名,在PATH中寻找文件execv():v表示vector传参execvp()execvpe()这个e表示环境变量,缺省传char** environexecve()
可能产生的疑问
对于环境变量[可能产生的疑问]
内核怎么完成程序替换的?——对原来进程的代码和数据进行覆盖是处理,并没有创建新的task_struct,exec完,原始代码就不存在了,后续代码不运行。
对于exec的函数返回值——只有exec失败的返回值有意义(为-1),如果成功,直接运行新代码了,返回值没意义了。
5万+

被折叠的 条评论
为什么被折叠?



