Linux:进程

发布时间:2026/6/30 5:30:59

Linux:进程 Linux进程冯诺依曼体系结构计算机如笔记本、服务器等大部分都遵守冯诺依曼体系结构。计算机都是由一个个的硬件组成的。输入设备包括键盘鼠标扫描仪等。中央处理器(CPU)含有运算器和控制器等。输出设备显示器打印机等。这里的存储器指的是内存。不考虑缓存情况这里的CPU能且只能对内存进行读写不能访问外设(输入或输出设备)(数据层面)。外设要输入或者输出数据也只能写入内存或者从内存中读取。即所有设备都只能直接和内存打交道。操作系统任何计算机系统都包含一个基本的程序集合称为操作系统(Operator System 即OS)。操作系统包括内核进程管理内存管理文件管理驱动管理等其他程序函数库shell程序等。设计OS的目的对下与硬件交互管理所有软硬件资源对上为应用程序提供一个良好的执行环境。在整个计算机软硬件架构中操作系统的定位是一款纯正的“搞管理”的软件。总体思路是先描述再组织。计算机先把数据信息封装成结构体再用高效数据结构把这些结构体对象组织起来从而管理硬件。在开发角度操作系统对外会表现为一个整体但是会暴露自己的部分接口供上层开发使用这部分由操作系统提供的接口叫做系统调用。系统调用在使用上功能比较基础对用户的要求相对也比较高所以有心的开发者可以对部分系统调用进行适度封装从而形成库。有了库就很有利于更上层用户或者开发者进行二次开发。进程概念课本概念程序的一个执行实例正在执行的程序等。内核观点担当分配系统资源CPU时间内存的实体。可执行程序先在磁盘中执行时加载到内存内存中对应有一个对象记录可执行程序各种属性这些对象由数据结构管理。进程 内核数据结构 可执行程序的代码和数据。描述进程PCB与task_struct进程信息被放在⼀个叫做进程控制块的数据结构中可以理解为进程属性的集合即PCB(process control block)。Linux下的PCB是task_struct。在 Linux 中描述进程的结构体叫做 task_struct 。task_struct 是 Linux 内核的⼀种数据结构类型它会被装载到RAM(内存)里并且包含着进程的信息。task_struct包含的内容如下标示符描述本进程的唯一标示符用来区别其他进程。状态任务状态退出代码退出信号等。优先级相对于其他进程的优先级。程序计数器程序中即将被执行的下一条指令的地址。内存指针包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针。上下文数据进程执行时处理器的寄存器中的数据。I/O状态信息包括显示的I/O请求分配给进程的I/O设备和被进程使用的文件列表。记账信息可能包括处理器时间总和使用的时钟数总和时间限制记账号等。其他信息……所有运行在系统里的进程都以 task_struct双链表的形式存在于内核。查看进程man man查看手册。1号手册查看可执行程序和命令2号手册查看系统调用3号手册查看库函数。man getpid查看getpid的手册getpid是系统调用。进程的pid是递增的。哪个进程调用getpid就可以得到哪个进程的标示符。我们写的程序运行起来也是进程为了验证准备如下代码和Makefile。运行后便能看到进程标示符。ps axj查看当前系统里进程有哪些。执行myprocess再打开一台机器ps axj | grep myprocess可以查看myprocess的进程。Linux中想同时执行两个命令可以用;和链接两个命令。如查看某个进程时带上第一行的表头ps axj | head -1;ps axj | grep myprocess或ps axj | head -1 ps axj | grep myprocess。杀掉进程可以用Ctrl C也可以用kill -9 进程pid。进程的信息可以通过/proc系统文件夹查看。名称为数字的目录保存进程的信息。运行myprocessls /proc/进程pid -dl可以找到对应目录得到对应进程的信息。把进程杀掉后就找不到对应目录了。ls /proc/进程pid -l查看进程信息。其中重点是exe和cwd。exe记录了这个进程对应可执行文件的绝对路径。运行myprocess在另一台Linux机器中删除myprocess进程仍在跑。但是此时会有警告。这说明我们删除的是磁盘上的myprocess进程启动时myprocess的拷贝已经在内存了所以程序仍在运行。cwd即current work dir 记录当前工作目录。如果在myprocess.c加一行代码fopen(hello.txta)进程就能在当前路径下创建该文件。更改进程所处当前路径查手册man chdir。chdir也是系统调用。输入如下代码更改进程路径。可以看到进程的当前工作路径被更改那么文件hello.txt也被创建在/home/dx目录下。与getpid()类似getppid()能取得进程的父进程的pid。可以看到父进程的pid是不变的。这个进程的父进程是bashOS会给每一个登录用户分配一个bash。bash就是命令行解释器其本质也是一个进程。像命令ls、top、pwd、mkdir、touch等都是bash的子进程。通过系统调用创建进程——fork初步man fork查看手册fork用于创建子进程是一个系统调用。输入如下代码。因为fork创建了子进程父进程和子进程两个执行流从fork()这一行向下执行所以执行完fork后原本的进程继续执行第二个printf创建的子进程也会执行第二个printf。fork()创建子进程就要创建子进程的PCB子进程的PCB拷贝自父进程PCB二者大部分进程属性相同子进程也会指向父进程指向的数据和代码。fork()的返回值如下。如果创建子进程成功子进程的pid返回给父进程0返回给子进程。为什么fork()返回给父子进程不同的返回值因为父进程有多个子进程子进程只有一个父进程类似多叉树父进程为了管理子进程所以要接收子进程的id所以给父进程返回子进程的pid给子进程返回0。为什么一个函数会返回两次调用fork()fork内部完成子进程的创建两个进程各自独立执行后续代码拿到不同返回值分别完成一次函数返回。进程具有独立性。比如父进程挂掉不会影响子进程。代码只能读不怕被修改。但数据父子进程共享父子进程任何一方修改数据OS会在底层内存中把被修改的数据拷贝一份然后让进程去修改这个拷贝即写时拷贝。上述代码定义全局变量gval在子进程修改gval然后父子进程分别打印gval。可以看到对于同一个全局变量gval子进程的gval会变化而父进程的gval不变这就是发生了写时拷贝。进程状态状态在kernel源代码里的定义/* *The task state array is a strange bitmap of *reasons to sleep. Thus running is zero, and *you can test for combinations of others with *simple bit tests. */staticconstchar*consttask_state_array[]{R (running),/*0 */S (sleeping),/*1 */D (disk sleep),/*2 */T (stopped),/*4 */t (tracing stop),/*8 */X (dead),/*16 */Z (zombie),/*32 */};R运行状态(running)并不意味着进程⼀定在运行中它表明进程要么是在运行中要么在运行队列里。S睡眠状态(sleeping)即阻塞状态。意味着进程在等待事件完成这里的睡眠有时候也叫做可中断睡眠interruptible sleep。S状态的进程可以被OS杀掉。D磁盘休眠状态(Disk sleep)也是一种阻塞状态有时候也叫不可中断睡眠状态uninterruptible sleep在这个状态的进程通常会等待IO的结束。OS不能杀掉D状态的进程。T停止状态(stopped)可以通过发送 SIGSTOP 信号给进程来停止T进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。X死亡状态(dead)这个状态只是一个返回状态你不会在任务列表里看到这个状态。名词解释阻塞进程等待 IO 设备、互斥锁、信号等资源就绪主动放弃 CPU驻留内存并加入对应等待队列。挂起系统物理内存资源紧张时内核将长时间不活跃的进程完整换出至 swap 交换分区进程原有状态不变必须换回内存后才能参与 CPU 调度。输入如下代码gcc编译后运行再另开一台Linux机器输入命令查看myprocess进程while :; do ps axj | head -1; ps axj | grep myprocess; sleep 1; done。从STAT可以看到myprocess进程的状态为睡眠状态但我们的明明正在运行程序这是因为CPU 速度远快于终端输出 IO绝大多数时间内进程都在等显示器输出极少时刻短暂处于 R 状态ps抓拍几乎只能看到 S。注释掉printf即可看到myprocess进程处于R运行状态。状态的加号表示进程在前台运行这种情况下会阻塞命令行输入./myprocess 可以让进程在后台运行此时命令行能正常输入状态不带加号杀掉进程需要使用命令kill -9 pid。观察S状态使用如下代码。利用scanf阻塞进程使进程不被调度等待键盘输入。S状态可以被OS杀掉。t状态是暂停状态debug模式下调试器在断点处暂停代码运行本质是进程被暂停了。T状态进程不是被调试器暂停而是被用户使用键盘CtrlZ暂停。kill -19 pid也可以暂停进程使其为T状态。kill -18 pid使进程从T状态恢复为运行状态此时需要kill -9 pid杀掉进程。kill l查看对应的选项。Z状态即僵尸状态是一个比较特殊的状态当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。僵尸进程会以终止状态保持在进程表中并且会一直在等待父进程读取退出状态代码。所以只要子进程退出父进程还在运行但父进程没有读取子进程状态则子进程进入Z状态。可以看到有两个./test分别是父进程和子进程。当子进程结束父进程仍在运行子进程就变为Z状态。父进程如果一直不读取那子进程就一直处于Z状态。维护退出状态本身就是要用数据维护也属于进程基本信息所以保存在task_struct(PCB)中即Z状态⼀直不退出PCB⼀直都要维护。如果子进程一直不被回收则会导致内存泄漏。父进程先退出子进程就称之为孤儿进程。此时子进程的父进程为1号进程即systemd进程子进程退出后就由systemd进程回收。子进程变成孤儿进程后就变为了后台进程需要用kill -9 pid才能杀掉。top可以看到systemd进程。进程优先级CPU资源分配的先后顺序就是指进程的优先级。优先级高的进程有优先执行权利。配置进程优先级对多任务环境的Linux很有用可以改善系统性能。优先级是一个数字是PCB的一个属性值越低优先级越高。优先级可以变化但变化幅度不会太大。UID即user id每个用户都有自己的UIDLinux靠UID识别用户。ls -nl可以查看到用户UID。PRI进程的优先级默认为80。范围[60,99]。优先级设置不合理会使优先级低的进程长时间得不到CPU资源导致进程饥饿。NI即nice值进程优先级的修正数据。PRI 80 NI一般通过修改NI来修改优先级。使用top命令输入r表示更改NI输入进程pid再输入NI的新值如下图所示PRI也就被更改了。按q退出。NI更改为负值时需要su -切换成root用户才能操作。竞争性系统进程数目众多而CPU资源只有少量甚至1个所以进程之间是具有竞争属性的。为了高效完成任务更合理竞争相关资源便具有了优先级。独立性多进程运行需要独享各种资源多进程运行期间互不干扰。并行多个进程在多个CPU下分别同时运行这称之为并行。并发多个进程在一个CPU下采用进程切换的方式在一段时间之内让多个进程都得以推进称之为并发。进程切换与调度CPU上下文切换其实际含义是任务切换或者CPU寄存器切换。当多任务内核决定运行另外的任务时它会保存正在运行任务的当前状态即CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器并开始下一个任务的运行。时间片分时操作系统中CPU 分配给单个任务连续运行的固定时间长度本质是计数器是时间片轮转调度的核心。CPU 轮流给就绪队列里每个进程分配一段时间时间耗尽就触发上下文切换换下一个进程运行实现宏观上多任务同时运行。时间片长短可配置片长越大上下文切换次数越少、系统开销越小响应速度变慢。Linux2.6内核进程O(1)调度队列调度的目的是挑队列挑进程。每个CPU有一个runqueue多核则需做负载均衡。active指针永远指向活动队列expired指针永远指向过期队列。活动队列时间片还没有结束的所有进程都按照优先级放在该队列。nr_active表示总共有多少个运行状态的进程。queue[140]中一个元素就是一个进程队列相同优先级的进程按照FIFO规则进行排队调度数组下标就是优先级。bitmap[5]为位图一共140个优先级一共140个进程队列为了提高查找非空队列的效率就可以用5*32160个bit位表示队列是否为空这样便可以大大提高查找效率。过期队列与活动队列结构一样过期队列上放置的进程都是时间片耗尽的进程当活动队列上的进程都被处理完毕之后对过期队列的进程进行时间片重新计算。调度流程从 active 队列按 bitmap 取最高优先级进程运行队头进程时间片耗尽后将进程移入 expired 队列active 队列上的进程会越来越少expired 队列上的进程会越来越多active 无进程时互换 active/expired 指针继续调度。在系统中通过位图查找一个最合适调度的进程的时间复杂度是一个常数不随着进程增多而导致时间成本增加我们称之为进程调度O(1)算法。命令行参数和环境变量环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。例如我们在编写C/C代码的时候在链接时从来不知道我们的所链接的动态静态库在哪里但是照样可以链接成功生成可执行程序原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途还有在系统当中通常具有全局特性。在编写C/C代码时main函数也是有参数的int main(int argc,char* argv[])。argv是一个字符指针数组argc为数组长度。准备如下代码和Makefile可以观察到argv的用途。可以看到命令行被空格分割成字符串argv的指针指向这些字符串。argv[0]为程序名argv的元素都是命令行参数。命令行参数的用途是让一个程序可以通过不同的选项实现不同的子功能指令选项也就是以此实现的。当我们使用Linux指令时进程会有argc表来支持选项功能。要执行一个程序必须先找到它。系统中存在环境变量来帮助系统找到二进制文件。系统不能从默认路径找到我们的code所以我们要./code才能运行程序。系统一般会从/usr/bin下查找指令如果把code拷贝到该目录下运行时输入code即可。Linux中环境变量PATH记录了默认指令搜索路径。env查看所有环境变量。echo $环境变量可以根据环境变量名查看一个环境变量。其中冒号是分隔符分隔出路径当输入一个指令时系统先从第一个路径下搜索没找到则去下一个路径搜索。所以如果把code的路径添加到PATH中使用时也可以直接用code。直接赋值PATHcode所在路径会覆盖环境变量的内容导致其他指令无法正常使用。PATH是在内存上的退出后重新登录PATH就会恢复。PATH$PATH:code路径则不会覆盖在后面接上code路径。bash有两张表命令行参数表和环境变量表环境变量表是字符指针数组指向环境变量的字符串如PATH/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dx/.local/bin:/home/dx/bin输入指令ls -lbash先根据命令行参数表解析再去环境变量表找到环境变量按照路径搜索指令。环境变量最开始是从配置文件中来的。配置文件在家目录下.bash_profile和.bashrc。cd ~打开这两个配置文件。把code所在路径放到配置文件中退出后重新登录就能永久地输入code直接运行code。常见的环境变量HOME: 指定用户的主工作目录即用户登陆到Linux系统中时默认的目录。环境变量USER记录当前用户LOGNAME记录登录用户。HISTSIZE最多记录历史命令条数如上图为3000即最多记录历史命令3000条。HOSTNAME当前主机名。SSH_TTY当前是哪个设备。PWD记录当前Shell所在路径。OLDPWD记录上一次Shell所在路径所以cd -能回到上一次所在路径。export设置一个新的环境变量。unset 环境变量删除环境变量。main函数最多有3个参数即main(int argc,char* argv[],char* env[])。当我们写的代码编译为可执行程序并运行后它的父进程bash会传过来命令行参数表和环境变量表即argv[]env[]这两个指针数组的最后一个元素都为空指针。使用如下代码便可以看到环境变量。用export添加环境变量重新运行code能显示出我们添加的环境变量进一步说明我们添加的环境变量能被子进程拿到。环境变量可以被子进程继承。函数getenv获取环境变量找到环境变量则返回其地址否则返回空。我们可以写一个只有自己能执行的文件即使是root也不能执行。environ是一个全局字符指针数组用来存放当前进程全部环境变量。bash会记录两套变量环境变量和本地变量本地变量不会被子进程继承只会在bash内部使用。set显示环境变量和本地变量。unset删除本地变量。此时用export i也可以把 i 添加到环境变量。export是内建命令使用时不需要创建子进程bash自己调用函数或系统调用完成。程序地址空间这是我们在学习C/C时接触到的空间分布图。准备如下代码可以看到对于全局变量gval父子进程输出的地址一样但变量内容不一样。变量内容不一样所以父子进程输出的变量绝对不是同一个变量。但地址值是一样的说明该地址绝对不是物理地址。在Linux地址下这种地址叫做虚拟地址。我们在用C/C所看到的地址全部都是虚拟地址。物理地址用户一概看不到由OS统一管理。一个进程有一个虚拟地址空间和一套页表页表用做虚拟地址和物理地址物理内存的映射。虚拟地址空间的宽度是1字节32位机器下有 232个地址即4G64位下有 264个地址。32位下0x0 ~ 0xFFFFFFFF(4G)为虚拟地址空间0x0 ~ 0xBFFFFFFF(3G用户空间)为进程地址空间。描述Linux下进程的地址空间的所有的信息的结构体是mm_struct内存描述符。每个进程只有一个mm_struct结构在每个进程的 task_struct 结构中有一个指向该进程的mm_struct结构体指针。创建全局变量g_val其在父进程的地址为0x111111。调用fork()创建子进程子进程创建它的task_struct子进程的mm_struct发生浅拷贝与父进程指向同一个mm_struct对象。父子各自拥有一套独立页表但两张页表中虚拟地址0x111111都映射到同一块物理内存 0x112233页面被标记为只读实现地址空间资源共享。当父子进程的任意一方修改g_val的值时内核分配一块全新物理内存0x223344把原物理页上g_val的数据完整拷贝到新物理页。之后把修改g_val的进程的页表的映射关系更改内核为子进程创建专属、独立的 mm_struct父子不再共享地址空间管理结构。mm_struct通过记录起始地址和终止地址来实现对虚拟地址空间的区域划分。如mm_struct中有成员变量unsigned long start_code记录代码段的起始地址unsigned long end_code记录代码段的终止地址。由 task_struct 到 mm_struct 进程地址空间的分布情况structmm_struct{/*...*/structvm_area_struct*mmap;/* 指向虚拟区间(VMA)链表 */structrb_rootmm_rb;/* 红黑树 */unsignedlongtask_size;/*具有该结构体的进程的虚拟地址空间的⼤⼩*//*...*/// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。unsignedlongstart_code,end_code,start_data,end_data;unsignedlongstart_brk,brk,start_stack;unsignedlongarg_start,arg_end,env_start,env_end;/*...*/}每一个进程都会有自己独立的 mm_struct 操作系统需要将这么多进程的 mm_struct 组织管理起来。虚拟空间的组织方式有两种当虚拟区间较少时采取链表由mmap指针指向这个链表。当虚拟区间多时采取红黑树进行管理由mm_rb指向这棵树。Linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域(VMA)由于每个不同质的虚拟内存区域功能和内部机制都不同因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。上面提到的两种组织方式使用的就是vm_area_struct结构来连接各个VMA方便进程快速访问。structvm_area_struct{unsignedlongvm_start;//虚拟内存区起始unsignedlongvm_end;//虚拟内存区结束structvm_area_struct*vm_next,*vm_prev;//前后指针structrb_nodevm_rb;//红黑树中的位置unsignedlongrb_subtree_gap;structmm_struct*vm_mm;//所属的 mm_structpgprot_tvm_page_prot;unsignedlongvm_flags;//标志位struct{structrb_noderb;unsignedlongrb_subtree_last;}shared;structlist_headanon_vma_chain;structanon_vma*anon_vma;conststructvm_operations_struct*vm_ops;//vma对应的实际操作unsignedlongvm_pgoff;//文件映射偏移量structfile*vm_file;//映射的⽂件void*vm_private_data;//私有数据atomic_long_tswap_readahead_info;#ifndefCONFIG_MMUstructvm_region*vm_region;/* NOMMU mapping region */#endif#ifdefCONFIG_NUMAstructmempolicy*vm_policy;/* NUMA policy for the VMA */#endifstructvm_userfaultfd_ctxvm_userfaultfd_ctx;}__randomize_layout;页表除了做虚拟地址和物理地址的映射还有记录对物理地址的读写等权限。例如代码const char* s Hello World; *sh常量字符串是不可以被修改的当我们通过页表去访问物理内存打算修改s时系统会根据页表中对应的权限进行阻止。地址空间和页表由OS创建并维护凡是想使用地址空间和页表进行映射也⼀定在OS的监管之下进行访问。这样就保护了物理内存中的所有的合法数据包括各个进程以及内核的相关有效数据。因为有虚拟地址空间的存在和页表的映射存在物理内存中可以对未来的数据进行任意位置的加载。物理内存的分配和进程的管理就可以做到脱钩实现进程管理模块和内存管理模块的解耦合。因为有虚拟地址空间的存在所以我们在C、C上newmalloc空间的时候其实是在虚拟地址空间上申请物理内存甚至可以一个字节都不分配。当真正进行对物理地址空间访问的时候才执行内存的相关管理算法帮助申请内存构建页表映射关系延迟分配这是由操作系统自动完成用户包括进程完全0感知。因为页表的映射的存在程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进行映射在进程视角所有的内存分布都可以是有序的。

相关新闻