【Linux入门】从0开始认识进程 本文主要内容包括认识冯诺依曼体系结构理解现代计算机的基本工作原理学习操作系统的概念与定位理解管理的本质思想深入理解进程概念认识 PCB进程控制块及其作用学习进程状态掌握进程创建过程理解僵尸进程和孤儿进程的形成原因及危害了解 Linux 进程调度机制掌握进程优先级相关概念理解进程的竞争性与独立性区分并发与并行理解进程切换过程认识 Linux 2.6 Kernel 中 O(1) 调度算法架构学习环境变量相关知识掌握常见环境变量及相关命令熟悉 getenv、setenv 等环境变量操作函数理解 C 语言内存空间分布规律了解进程内存映像与应用程序之间的区别认识虚拟地址空间及其存在意义一、冯诺依曼体系结构冯诺依曼体系结构是现代计算机最常见的组织方式。我们平时接触的笔记本、服务器大体都符合这套模型。它把计算机划分成几个基本部分输入单元比如键盘、鼠标、扫描仪中央处理器 CPU负责运算和控制输出单元比如显示器、打印机存储器也就是内存这里要特别注意这里说的存储器指的是内存不是硬盘。冯诺依曼体系最核心的规则是CPU 只能直接和内存打交道外设也只能先把数据写入内存或者从内存取数据所有设备的数据传输本质上都绕不开内存也就是说所有设备都只能直接和内存交互。注意理解冯诺依曼不能只停留在硬件名词上还要能把它和软件数据流联系起来。比如 QQ 聊天时你输入消息先进入内存CPU 处理消息内容消息通过网络协议发出去对方收到后数据先落到内存再交给对应程序读取如果发送文件过程也是一样的只是传输的数据量更大路径更长。二、操作系统(Operator System)2-1 概念操作系统是整个计算机系统中最基础的一类软件。广义上说它包含两部分内核比如进程管理、内存管理、文件管理、驱动管理其他系统级程序比如函数库、shell 等操作系统对外看起来像一个整体但内部其实分工明确。它最重要的任务不是替代应用程序而是给应用程序提供运行环境。2-2 设计OS的目的操作系统存在的意义可以分成两个方向对下和硬件交互管理所有软硬件资源对上给用户程序提供一个稳定、统一、好用的执行环境这意味着操作系统既要管硬件又要服务上层程序。2-3 核心功能操作系统在整个软硬件架构中的定位可以概括成一句话它是一款专门负责管理的软件它要管理的对象很多比如CPU内存硬盘文件进程设备所以理解操作系统最重要的不是把它想成一个神秘的大黑盒而是把它看成一个管理者。2-4 如何理解 管理管理这件事最容易理解的方法就是拿现实中的组织结构做类比。比如学校里学生是被管理对象辅导员负责日常管理校长负责更高层次的组织和决策管理一个对象通常分两步先描述它再组织它操作系统管理硬件和进程也遵循同样的思路用结构体描述对象属性用链表、树等数据结构组织这些对象描述起来用结构体组织起来用链表或其他高效数据结构2-5 系统调用和库函数概念操作系统不会把所有能力都直接暴露给用户程序。它会提供一部分基础接口给上层使用这些接口叫做系统调用。系统调用的特点是功能比较底层使用门槛高接口偏基础为了方便开发很多系统调用会被进一步封装成更好用的库函数。简单理解系统调用负责和内核直接打交道库函数负责把复杂接口包装得更容易用三、进程3-1 基本概念与基本操作教材对进程的定义程序的一个执行实例也就是正在执行的程序。从内核的角度看进程是分配系统资源的实体现在可以先把进程理解成内核数据结构 task_struct 程序代码和数据也就是说进程不是单纯的一段代码而是代码和内核管理信息的组合。3-1-2 描述进程-PCB进程的信息不会散落在内核中而是统一放在一个专门的数据结构里这个结构叫做进程控制块 PCB。在 Linux 中PCB 对应的就是task_struct。task_struct 里会包含进程的很多属性如进程标识符状态优先级程序计数器内存指针上下文数据I/O 状态信息记账信息可以把它理解成进程的档案袋进程要被管理先得有档案。3-1-3 task_structLinux 内核中进程就是靠 task_struct 描述的。它会被装载到内存里里面保存了进程的完整信息。常见字段为标识符区分不同进程状态表示当前处于运行、睡眠、停止等哪种状态优先级决定调度先后程序计数器下一条将要执行的指令地址内存指针代码段、数据段以及共享内存的相关信息上下文数据寄存器现场I/O 状态信息文件列表、I/O 请求等记账信息CPU 时间、执行时间限制等所有进程在内核里还会以双链表的形式组织起来方便统一管理。3-1-4 查看进程查看进程信息有两种常见方式直接看/proc文件系统使用top、ps这类用户态工具比如 PID 为 1 的进程信息可以查看/proc/1目录。3-1-5 通过系统调用获取进程标识符进程最重要的两个标识符是PID进程号PPID父进程号#includestdio.h#includesys/types.h#includeunistd.hintmain(){printf(pid: %d\n,getpid());printf(ppid: %d\n,getppid());return0;}这两个接口非常常用尤其在父子进程、守护进程、进程树分析里。3-1-6 通过系统调用创建进程-fork初识创建子进程要用fork。基本概念fork有两个返回值父子进程代码共享数据各自拥有一份采用写时拷贝机制#includestdio.h#includesys/types.h#includeunistd.hintmain(){intretfork();printf(hello proc : %d!, ret: %d\n,getpid(),ret);sleep(1);return0;}通常会结合if做分流#includestdio.h#includesys/types.h#includeunistd.hintmain(){intretfork();if(ret0){perror(fork);return1;}elseif(ret0)// 子进程{printf(I am child : %d!, ret: %d\n,getpid(),ret);}else// 父进程{printf(I am father : %d!, ret: %d\n,getpid(),ret);}sleep(1);return0;}注意为什么fork会有两个返回值返回值如何分别给父进程和子进程这两个问题后面会深入讲进程复制机制。3-2 进程状态3-2-1 Linux内核源代码Linux 内核里进程状态不是单一的而是有多种状态。常见状态包括R运行状态S睡眠状态D磁盘休眠状态T停止状态X死亡状态Z僵尸状态3-2-2 进程状态查看查看进程状态常用命令是ps auxps axj其中a显示终端上的所有进程x显示没有控制终端的进程j显示进程组 ID、会话 ID、父进程 ID 等作业控制信息u以用户为中心显示详细信息3-2-3 Z(zombie)-僵尸进程它的形成过程是子进程已经退出父进程还活着父进程没有调用wait或waitpid回收子进程状态子进程就会进入 Z 状态僵尸进程本身不再运行但它的退出信息还留在进程表中等待父进程读取。例子#includestdio.h#includestdlib.h#includeunistd.hintmain(){pid_tidfork();if(id0){perror(fork);return1;}elseif(id0){printf(parent[%d] is sleeping...\n,getpid());sleep(30);}else{printf(child[%d] is begin Z...\n,getpid());sleep(5);exit(EXIT_SUCCESS);}return0;}僵尸进程危害僵尸进程会一直占着进程表中的一部分信息因为它的退出状态还要保留给父进程查看。这意味着PCB 相关信息不能立刻释放如果父进程长期不回收资源会被浪费大量僵尸进程会造成系统压力注意这类泄漏不是内存块忘了释放而是进程状态和控制块长期无法回收。3-2-5 孤儿进程如果父进程先退出子进程还在运行那么这个子进程就叫做孤儿进程。孤儿进程不会一直无人管理它会被 1 号进程也就是init或systemd进程领养后续由它负责回收。#includestdio.h#includeunistd.h#includestdlib.hintmain(){pid_tidfork();if(id0){perror(fork);return1;}elseif(id0){printf(I am child, pid : %d\n,getpid());sleep(10);}else{printf(I am parent, pid: %d\n,getpid());sleep(3);exit(0);}return0;}孤儿进程本身不是错误但要理解它的归宿是被系统接管。3-3 进程优先级3-3-1 基本概念CPU 资源分配的先后顺序就是进程的优先级。优先级高的进程通常更早获得执行机会。这在多任务系统里非常重要因为 CPU 资源有限进程很多必须通过优先级来协调竞争。3-3-2 查看系统进程在ps -l的输出中常见字段有UID执行者身份PID进程号PPID父进程号PRI实际执行优先级NInice 值3-3-3 PRI and NIPRI可以理解为真正的优先级值越小越优先执行。NI是 nice 值可以理解成优先级修正值。二者关系可以写成PRI(new)PRI(old)nice所以nice 值越小优先级越高nice 值越大优先级越低Linux 中 nice 的取值范围通常是-20到19。3-3-4 PRI vs NI区分PRI是进程优先级NI不是优先级本身NI是影响优先级的修正值也就是说nice 值只是调整项不是最终值。3-3-5 查看进程优先级的命令可以用top修改已有进程的 nice 值先输入top再按r输入进程 PID再输入新的 nice 值其他命令还有nicerenice相关系统函数#includesys/time.h#includesys/resource.hintgetpriority(intwhich,intwho);intsetpriority(intwhich,intwho,intprio);3-3-6 竞争、独立、并行、并发概念含义竞争性多个进程争抢有限 CPU 资源独立性多个进程互不干扰各自运行并行多个进程在多个 CPU 上同时运行并发多个进程在一个 CPU 上通过切换推进执行并行强调真正同时执行并发强调一段时间内都能推进。3-4 进程切换进程切换的本质是 CPU 上下文切换也就是保存当前任务的寄存器现场恢复下一个任务的寄存器现场CPU 转去执行新的任务注意时间片是分时系统里非常重要的概念。每个进程都会被分配一定的 CPU 时间片时间片到了就会被系统切走。3-4 Linux2.6内核进程O(1)调度队列Linux 2.6 时代的调度器采用过 O(1) 调度思想目标是查找最合适进程的时间复杂度尽量保持常数级。基本结构每个 CPU 对应一个 runqueue。如果有多个 CPU就要考虑负载均衡问题。调度队列分为两个核心数组active活动队列expired过期队列优先级划分普通优先级100 到 139实时优先级0 到 99普通进程最常见。活动队列时间片没有用完的进程都放在活动队列中。nr_active活动进程数queue[140]每个优先级一个队列同优先级按 FIFO 排队查找过程大致是从低下标开始遍历找到第一个非空队列取出队首进程执行为了提高效率还会配合 bitmap 记录哪些队列非空。过期队列时间片用完的进程会进入过期队列。当活动队列处理完后再把过期队列中的进程重新计算时间片交换 active 和 expired 指针继续下一轮调度。所以整个查找过程可以保持接近 O(1) 的效率。四、命令行参数和环境变量4-1 基本概念环境变量是操作系统中用来描述运行环境的一组参数。它有几个典型特点和系统配置有关具有全局属性会被子进程继承比如编译和链接时程序能找到动态库或静态库很大程度上就和环境变量有关。4-2 常见环境变量常见环境变量包括PATH命令搜索路径HOME当前用户主目录SHELL当前使用的 shell一般是/bin/bash4-3 查看环境变量方法查看某个环境变量echo$NAME比如echo $PATHecho $HOME关于 PATH 的理解如果一个程序不在系统默认路径里通常要写完整路径才能执行。把程序所在目录加入PATH后就可以直接运行。4-4 和环境变量相关的命令常见命令有echo查看变量值export导出环境变量env显示所有环境变量unset清除环境变量set显示 shell 变量和环境变量4-5 环境变量的组织方式每个程序启动时都会收到一张环境表。这张环境表本质上是一个字符指针数组每个元素指向一个环境字符串每个字符串都以\0结尾4-6 通过代码如何获取环境变量方法一通过命令行参数传入的 env#includestdio.hintmain(intargc,char*argv[],char*env[]){inti0;for(;env[i];i){printf(%s\n,env[i]);}return0;}方法二通过全局变量 environ#includestdio.hintmain(intargc,char*argv[]){externchar**environ;inti0;for(;environ[i];i){printf(%s\n,environ[i]);}return0;}environ没有包含在常规头文件中所以使用前要extern声明。4-7 通过系统调用获取或设置环境变量常用函数是getenvputenv示例#includestdio.h#includestdlib.hintmain(){printf(%s\n,getenv(PATH));return0;}getenv用来获取指定环境变量的值。4-8 环境变量通常是具有全局属性的环境变量可以被子进程继承。#includestdio.h#includestdlib.hintmain(){char*envgetenv(MYENV);if(env){printf(%s\n,env);}return0;}如果只设置MYENVhelloworld但不export程序通常拿不到。如果执行exportMYENVhello world再运行程序就能读取到。这说明导出的环境变量会进入环境表并被子进程继承。五、程序地址空间核心程序地址空间进程地址空间虚拟地址物理地址5-1 研究平台这里讨论的环境是kernel 2.6.3232 位平台地址空间布局和内核行为会受平台影响。5-2 程序地址空间查看验证不同区域的地址分布#includestdio.h#includeunistd.h#includestdlib.hintg_unval;intg_val100;intmain(intargc,char*argv[],char*env[]){constchar*strhelloworld;printf(code addr: %p\n,main);printf(init global addr: %p\n,g_val);printf(uninit global addr: %p\n,g_unval);staticinttest10;char*heap_mem(char*)malloc(10);char*heap_mem1(char*)malloc(10);char*heap_mem2(char*)malloc(10);char*heap_mem3(char*)malloc(10);printf(heap addr: %p\n,heap_mem);printf(heap addr: %p\n,heap_mem1);printf(heap addr: %p\n,heap_mem2);printf(heap addr: %p\n,heap_mem3);printf(test static addr: %p\n,test);printf(stack addr: %p\n,heap_mem);printf(stack addr: %p\n,heap_mem1);printf(stack addr: %p\n,heap_mem2);printf(stack addr: %p\n,heap_mem3);printf(read only string addr: %p\n,str);for(inti0;iargc;i){printf(argv[%d]: %p\n,i,argv[i]);}for(inti0;env[i];i){printf(env[%d]: %p\n,i,env[i]);}return0;}通过打印可以看出代码段有自己的地址全局变量有自己的地址堆和栈分布不同字符串常量在只读区argv 和 env 也都有自己的位置这说明进程内存布局是有规律的。5-3 虚拟地址如#includestdio.h#includeunistd.h#includestdlib.hintg_val0;intmain(){pid_tidfork();if(id0){perror(fork);return0;}elseif(id0){g_val100;printf(child[%d]: %d : %p\n,getpid(),g_val,g_val);}else{sleep(3);printf(parent[%d]: %d : %p\n,getpid(),g_val,g_val);}sleep(1);return0;}输出会出现一个很关键的现象父子进程变量地址一样但变量值不一样这说明这个地址不是物理地址它是虚拟地址所以我们在 C 和 C 里看到的地址全部都是虚拟地址。物理地址对用户是不可见的由操作系统统一管理。5-4 进程地址空间以前常说程序地址空间但更准确的说法其实是进程地址空间。原因每个进程都有自己独立的地址空间同一个虚拟地址在不同进程里可以映射到不同物理地址进程之间互不干扰进程地址空间由操作系统通过页表进行映射和管理。描述进程地址空间的核心结构体是mm_struct内存描述符在task_struct中有一个指向mm_struct的指针。5-5 虚拟内存管理task_struct和mm_struct的关系可以理解为task_struct管进程整体mm_struct管进程地址空间mm_struct描述的是整个用户空间。每个进程都有自己的mm_struct这样才能拥有独立地址空间。常见字段包括mmap指向虚拟区间链表mm_rb红黑树根task_size进程虚拟地址空间大小start_code、end_code代码段范围start_data、end_data数据段范围start_brk、brk堆区范围start_stack栈区起始arg_start、arg_end命令行参数区域env_start、env_end环境变量区域vm_area_structLinux 用vm_area_struct表示一个独立的虚拟内存区域也叫 VMA。一个进程通常会有多个 VMA分别表示代码区数据区堆区栈区文件映射区共享内存区vm_area_struct的一些典型字段vm_start起始地址vm_end结束地址vm_next、vm_prev链表前后指针vm_rb红黑树节点vm_mm所属的 mm_structvm_flags标志位vm_file映射文件vm_private_data私有数据5-6 为什么要有虚拟地址空间如果程序直接操作物理内存会有什么问题?1. 安全风险如果每个进程都能直接访问物理内存那么程序可以随便读写系统内存木马和病毒更容易破坏系统进程之间也更容易互相干扰显然不安全。2. 地址不确定程序编译好后存放在磁盘上运行时才被加载到内存。如果直接用物理地址那么每次装载的位置都可能不同第一次运行内存可能很空第二次运行内存已经有很多程序了地址每次都可能变化这样程序就很难稳定运行。3. 效率低下如果直接使用物理内存进程会以整体块的形式管理。当内存不足时想把不常用的程序换出到磁盘就得整块搬移效率很低。虚拟地址空间的好处有了虚拟地址空间和页表机制之后问题就解决了很多进程看起来都有独立的地址空间进程之间互不干扰物理内存可以被统一调度内存管理和进程管理可以解耦malloc和new时未必立刻分配物理内存需要访问时才真正触发分配这叫延迟分配虚拟地址空间的本质是让进程看到一个稳定、连续、安全的内存空间总结计算机所有设备最终都要和内存打交道操作系统的核心任务是管理进程是操作系统管理资源的基本单位task_struct 是进程的核心描述结构fork 可以创建子进程进程有运行、睡眠、僵尸、孤儿等状态优先级和调度决定进程执行顺序环境变量会被进程继承程序看到的是虚拟地址不是物理地址虚拟地址空间让进程更安全、更稳定、更高效完