OS进程管理 目录进程是什么进程的创建linux下进程的状态进程状态转换的本质进程优先级进程切换进程调度进程地址空间虚拟地址空间环境变量命令行参数shell指令的本质本地变量linux的命令行关于环境变量和本地变量的命令main函数的返回值进程的终止进程等待进程程序替换进程是什么进程是运行起来的程序它不仅包含程序本身还包含OS管理一个程序运行所描述出来的结构体——PCB(包含进程调度管理所需要的各种属性以及程序本身的属性)因此进程 内核数据结构程序代码和数据。在linux中PCB就是task_struct进程的创建一般来说进程都是由另一个进程创建的因为计算机上第一个进程必然至少是操作系统本身在shell命令行中所有的进程都是由shell进程创建的这些进程的父进程都是shell。下面我们使用系统接口来创建多个子进程void sub_process()//这是让子进程执行的任务 { cout我是子进程endl; } int main() { vectorpid_t mysubprocess;//保存该进程创建的所有子进程的pid for(int i 0;i10;i) { pid_t id fork();//fork函数用来创建一个子进程对于子进程他返回0对于父进程他返回子进程的pid if(id 0)sub_process(); else mysubprocess.push_back(id); } }fork做了什么创建一个新的task_struct以及新的虚拟地址空间和页表并把父进程的task_struct和虚拟地址空间和页表的内容拷贝过来一份这就是为什么子进程执行的代码和父进程是同一份而且子进程执行的位置不是从头开始[task_struct保存了当前执行的指令的位置],然后把子进程pid等个性化的属性修改了并将子进程的task_struct挂到调度队列中去这样就有两个进程在运行了。fork为什么有两个返回值fork函数的最后一条语句是“return id;”而在这之前新的子进程已经被创建出来父子进程都会执行“return id;”因此父子进程会各自返回一次。为什么对于父子进程返回的id不是相同的在 x86-64 架构Linux下系统调用的返回值存放在%raxEAX/RAX寄存器中。内核在切换回用户态之前会手动修改这两个进程的寄存器上下文对于父进程内核将子进程的 PID进程ID写入父进程的%rax寄存器。对于子进程内核将0写入子进程的%rax寄存器。之后父进程和子进程各自恢复运行读取自己%rax里的值。于是你看到的就是同一个函数fork()返回了两个完全不同的数字。linux下进程的状态R(running)运行状态即进程PCB在调度队列里。S(sleeping)休眠状态即进程在阻塞等待某一事件的发生。可被信号中断。D(disk sleeping)休眠状态的一种读写磁盘的时候变为D状态但他是不可中断的被中断的话可能造成数据丢失尤其是在银行系统中。T(stopped)进程做了非法但是不致命的操作被OS暂停。t(tracing stop)调试的时候进程被追踪在断点处停下。Z(zombie)进程任务执行完了但是PCB还不销毁维持进程退出状态也就是返回值它保存在PCB中以供父进程和OS获取执行结果。如果没有人获取执行结果就会一直处于这个状态甚至无法被KILL。而如果父进程挂掉OS会接管并回收其子进程孤儿进程。X(dead)进程真正结束了但可能不会把PCB立即销毁因为给其他新建进程使用就不用新开空间了。注磁盘的swap分区专门用于挂起正在阻塞的进程在内存不足的时候换入换出。严重内存不足甚至可以挂起运行的进程。在系统很卡的时候进程太多或者换入换出都解决不了了OS甚至会干掉进程main函数的返回值就是把子进程的执行结果返回放到子进程PCB里给父进程读取进程状态转换的本质进程状态转换在OS中本质上就是把进程PCB放到不同的队列也就是数据结构中管理起来阻塞等待的本质就是把PCB列入到设备关联的等待队列里。注同一个PCB可以处在不同的队列中因为linux中是专门用一个链接字段做链接的。只要每种队列都维护这个字段就可以进程优先级PRI的值越小进程优先级越高默认优先级是80.为什么不直接修改PR而是通过修改NI间接修改优先级因为有可能进程正在运行此时修改优先级的话就会导致优先级混乱比如为什么当前运行的进程的优先级比其他人低还能先运行。通过先修改NI等到进程重新列入到调度队列中时再计算修改PRI这样就可以避免二义性。进程切换进程切换核心就是保存上下文数据。cpu内有很多寄存器eaxebxeippcir等用于存放临时数据。有时候进程时间片到了还没执行完需要把整套寄存器的数据存起来老内核中是直接存在PCB中下一次调度的时候再恢复这些数据。这些瞬时数据也叫做上下文数据他们保存的是进程之前运行到的位置正在计算的中间值等重要数据有了这些下次进程切换回来的时候就可以接着运行。pc程序计数器,也叫eip存放下一条要执行的指令的地址指令也是二进制存储执行后pc就加上本条指令的长度从而指向下一条指令。ir存放的是当前正在执行的指令eflag有一个标志位表示计算是否溢出每次cpu执行了一条指令后就去问pc接下来执行什么指令得到结果后就把指令从pc指向的内存加载到ir根据ir中指令的长度更新pc然后开始执行ir中的指令。进程调度进程地址空间虚拟地址空间在OS中每个进程都有一个虚拟地址空间实际上就是一个结构体对象mm_struct它里面用unsigned long变量各个分区的起止位置分区情况不同的分区存放不同类型的数据进程并不是直接申请内存的而是向自己的虚拟地址空间申请空间再通过页表映射到实际内存的位置访问一个地址的时候也是通过虚拟地址页表映射访问。子进程继承父进程地址空间子进程会创建新的虚拟地址空间和页表但是会把父进程的相关内容拷贝下来因此父子进程代码共享数据共享只读。一但子进程修改数据OS会把子进程页表上该数据地址与实际地址的映射关系改变重新开辟一个数据空间把页表上的真实地址改成新空间的地址并把旧空间的数据拷贝到新空间写时拷贝。所以父子进程共享代码和数据但又是绝对独立的所以会出现同一个地址数据内容不同的情况因为我们取地址返回的是虚拟地址父子进程虚拟地址相同但是真实地址不同。为什么写时拷贝还要把原数据拷贝一份反正还要修改因为我们不一定是直接赋值还可能是x这种基于原来值的修改。关于页表虚拟地址空间如何初始化程序编译连接后是ELF格式通过这个格式OS可以获取每个分区的大小从而对虚拟地址空间的分区进行初始化并对页表的权限位和存在位初始化。申请堆空间的本质申请堆空间的本质就是扩大虚拟空间中堆空间的范围也就是申请虚拟内存而不是申请了实际内存实际内存可以延迟开辟使用的时候才开辟。为什么要有虚拟内存页表保护内存通过页表可以防止访问只读数据或者野指针访问将进程管理和内存管理解耦合让进程以统一视角看待内存进程看到的内存的分区都是同种格式虚拟地址空间的堆区堆区比较特殊当我们malloc几次之后总共申请的这些空间不是整体使用的因此堆虚拟地址空间内部又把每一次申请的堆空间的起止位置用链表管理起来以此区分。环境变量无论是linux中的命令行bash还是windows中的图形化界面都是为了方便我们对系统进行操作而启动的进程。这些进程会读取环境变量相关的配置文件将环境变量导入到他们的进程地址空间或者从更前面创建的系统进程继承环境变量这里只是大致讲述原理也就是在上面所示的位置。由于用户的所有进程都是某个进程的子进程环境变量能被系统中的几乎所有进程获取并访问下面是环境变量的某些内容PATH这个环境变量掌管指令的默认查找路径这是为什么我们使用ls等user/bin中的系统指令时不用加路径。PWD这是当前工作目录这就是为什么进程PCB的cwd属性来源创建一个文件的时候会把文件创建在默认目录下USER登录的用户是谁这就是每个进程PCB中保存的uid的来源HOME:这个环境变量是记录当前用户的家目录。当我们登录一个bash时读取环境变量然后根据当前用户名把环境变量HOME修改为自己的家目录同时把bash进程的工作路径cwd设置为家目录。这就是为什么登录后的默认目录就是家目录。SHELL告诉用户启动的是哪个路径的bash。LOGNAME是执行身份。“su -”本质上是重新登录所以USER和LOGNAME都会同步改变。“su” 是转换执行身份不改变USER只改变LOGNAME。所以区分一个用户是以USER为主。HOSTNAME主机名LANGlanguage字符编码格式OLDPWD表示上一次改变之前的pwdcd - 就是进入OLDPWD路径。如何访问环境变量子进程的调用带参数的main函数它的参数就包括了环境变量的指针如果我们的main函数是带参数的父进程就会自动给我们传环境变量int main(int argc,char* argv[],char* envp[]) { cout envp[2] endl; }C语言库定义了一个全局变量environ他指向环境变量或者调用C库中getenv()函数extern char** environ; int main(int argc,char* argv[],char* envp[]) { cout environ[0] endl; }命令行参数shell指令的本质argc代表参数个数argv存放参数内容一个个字符串envp代表环境变量。他们都会被父进程自动传入。当我们在bash命令行输入指令的时候实际上bash拿到了一串字符串然后bash以空格为分隔符把字符串打散形成表argv,argv[0]一般是可执行程序的名字剩下的是不同的字符串在bash启动argv[0]进程时把这些不同的字符串传入该进程的main函数main函数可以根据字符串的不同执行不同的功能这就是指令和指令的选项的本质。本地变量本地变量是shell在堆上创建的一张变量表供shell进程自己使用它不像环境变量一样会被子进程保留子进程不会看到它。linux的命令行关于环境变量和本地变量的命令main函数的返回值我们都知道子进程执行结束后会进入Z状态没有人回收子进程他就会一直存在在这个状态下子进程的PCB会保存main函数的返回值回收子进程的进程可以拿到这个返回值。进程的终止在main函数中return。return只会退出当前函数。exit。exit在程序的任何位置调用都会终止进程并刷新语言级缓冲区。_exit。_exit在程序的任何位置调用都会终止进程但不会刷新语言级缓冲区。exit和_exit的区别就是_exit是系统调用接口而exit是c语言提供的函数内部封装了_exit。由于C语言或c先使用的缓冲区是语言级缓冲区不是由操作系统管理那么就造成一个现象exit会刷新语言级缓冲区而_exit已经是系统调用无法操作语言级缓冲区系统无法向上管理因此_exit退出时仍在语言级缓冲区的数据将无法打印出来除非手动调用fflush函数。而遇到\n会自动刷新缓冲区这两个调用就没什么区别了。进程等待子进程必须要被父进程等待回收才能完全销毁PCB销毁。而进程的PCB会记录进程的退出码和收到的信号。进程程序替换我们创建进程只介绍了fork函数创建子进程它创建出来的子进程默认就会执行父进程的代码那么我们可以不让子进程执行父进程的代码而执行全新的一个程序吗答案就是进程程序替换我们可以对一个进程进行进程程序替换把该进程对应的代码和数据都替换成新程序的代码和数据再修改一下PCB这样他执行的就是新程序的内容了这不是创建新进程而是用老进程的壳子执行新程序。ubuntu下查询进程和网络服务的命令ps auxa显示所有终端下的进程包括其他用户的进程。u以面向用户的格式输出提供更详细的信息如CPU、内存使用率。x显示没有控制终端的进程通常是后台运行的守护进程daemon。sudo netstat -tulnp-t显示 TCP 协议的连接-u显示 UDP 协议的连接-l仅显示处于 Listening监听 状态的端口即提供服务等待连接的-n以数字形式显示 IP 和端口不做 DNS 反向解析这样显示速度极快-p显示占用该端口的进程 PID 和程序名称