建立网站的市场价格,无锡教育论坛网站建设,电子商务网站建设第一章课后,汕头微网站Susan,在那命运月台前面#xff0c;再上车#xff0c;春天开始落叶.................................................................. 文章目录 前言
一、【认识进程】
1、【进程基本概念引入】
2、【进程的描述与组织——进程控制块#xff08;PCB#xff09;与进程… Susan,在那命运月台前面再上车春天开始落叶.................................................................. 文章目录 前言
一、【认识进程】
1、【进程基本概念引入】
2、【进程的描述与组织——进程控制块PCB与进程标识符PID)】
【Linux中的PCB————task_struct】
【task_struct的内容分类】
【task_struct中的PID】
【获取进程的PID和PPID】
二、【了解进程操作】
2.1、【查看进程】
1、通过系统目录查看
2、通过ps命令查看
3、通过top命令查看进程
2.2、【fork函数】
1、使用fork函数创建子进程
2、使用if进行分流
三、【进程状态】
1、【进程的6种状态】
创建状态New
就绪状态Ready
运行状态Running
阻塞状态Blocked
挂起状态suspend
终止状态Terminated
总结
2、【Linux中的进程状态】
运行状态-R
浅度睡眠状态-S
深度睡眠状态-D
暂停状态-T
死亡状态-X
3、【父子进程、僵尸进程和孤儿进程】
【父子进程总结】
【僵尸进程】
【孤儿进程】
四、【进程优先级】
1、【基本概念】
2、【查看优先级信息——PRI和NI】
3、【进程优先级的修改】
通过top命令更改进程的nice值
通过renice命令更改进程的nice值
4、【四个重要概念】
五、【环境变量】
1、【环境变量的介绍】
2、【测试PATH】
3、【测试HOME】
4、【测试SHELL】
5、【环境变量相关命令】
6、【环境变量的组织方式】
7、【获取环境变量】
【通过命令行参数获取环境变量】
【通过第三方变量environ获取】
【通过系统调用获取环境变量】
六、【进程地址空间】
1、【引入】
2、【虚拟内存——进程地址空间的介绍】
3、【进程空间存在的意义和几个重要的问题】
【进程空间存在的意义】
【解答几个重要的问题】
1、为什么fork函数会有两个返回值呢
2、为什么父进程接收子进程的PID而子进程返回0或-1
3、我们定义的id变量是如何做到存储两个值的
4、为什么数据要进行写时拷贝
5、为什么不在创建子进程的时候就进行数据的拷贝
6、代码会不会进行写时拷贝
7、为什么要有进程地址空间
8、现阶段应该如何理解进程创建
9、为什么子进程一开始不直接创建自己的物理内存空间而是进行写时复制呢
七、【Linux系统中进程的调度】
【概念引入】
【活动队列】
【过期队列】
【active指针和expired指针】
总结 前言
本篇博客主要是对进程概念进行讲解其中还涉及了环境变量进程地址空间等内容请耐心观看。 一、【认识进程】 1、【进程基本概念引入】 【我们先来看看课本上对进程是如何定义的】程序一般是放在物理磁盘中通过用户的执行来触发。触发后程序会加载到内存中成为一个个体这就是进程。通俗来讲进程可以为被理解为程序的一个执行实例 / 正在执行的程序。【再从内核的观点】 进程是参与分配系统资源主要指CPU时间内存的实体。 所以为了让操作系统管理进程操作系统(内核)会将此程序的执行者的权限与属性、程序的代码和所需的属性都会被加载到内存中同时为程序分配可能需要使用系统资源如内存、CPU时间、文件描述符等并在执行过程中进行管理和调度。操作系统还可能需要对程序进行一些初始化操作如设置程序的运行环境变量、加载动态链接库等。 【最后我们再看看进程在Linux系统中是如何定义】当触发一个事件时系统都会将它定义为一个进程并且给予这个进程一个标识符称为PID同时根据触发这个进程的用户与相关属性关系给予这个PID一组有效的权限设置。自此这个PID能够在系统上执行的操作就与这个PID的权限有关。 可见一个进程的产生离不开触发事件。那我们如何才能在系统中触发一个事件呢 其实很简单执行一个程序或者命令就可以触发一个事件进而产生一个进程。我们所说的“程序与命令”在操作系统中本质上就是一个二进制可执行文件。我们知道系统只认识二进制文件所以我们要让系统工作的时候当然是启动一个二进制可执行文件这个二进制文件就是程序它们通常放置在存储媒介中如硬盘、光盘、软盘、磁带等以物理文件的形式存在。 总结从进程本身和内核的角度来看进程是程序执行的实例同时也是内核调度和管理的基本单位它具有独立的地址空间、状态、标识符等特征并且可以通过内核提供的接口与系统进行交互和管理。 下面我们来看一个例子 我们都知道当我们写的代码进行编译链接后便会生成一个可执行程序这个可执行程序本质上是一个二进制文件是放在磁盘上的。当我们双击这个可执行程序将其运行起来时本质上是将这个程序加载到内存当中了因为只有加载到内存后CPU才能对其进行逐行的语句执行而一旦将这个程序加载到内存后我们就不应该将这个程序再叫做程序了严格意义上将应该将其称之为进程。 下面让我们注意这样一个问题程序在加载到内存中时“加载”是指什么以及这个过程是谁完成的 实际上这里的加载本质上就是拷贝是操作系统参与完成操作系统将程序从磁盘加载到内存就是将程序从磁盘拷贝到内存。 好了我们知道了进程是什么知道了是操作系统将可执行程序从磁盘拷贝到内存从而形成进程既然操作系统参与了进程的形成那么如果系统中有大量进程存在时必然少不了对进程的管理。 我们知道管理的本质是先描述再组织下面让我们看看操作系统中是如何对进程进行描述和组织的。 2、【进程的描述与组织——进程控制块PCB与进程标识符PID)】 我们要知道系统当中可以同时存在大量进程使用命令ps aux便可以显示系统当中存在的所有进程。 而当你开机的时候启动的第一个程序就是我们的操作系统即操作系统是第一个加载到内存的我们都知道操作系统是做管理工作的而其中就包括了进程管理。 但是系统内是存在大量进程的那么操作系统是如何对进程进行管理的呢 这时我们就应该想到管理的六字真言先描述再组织。操作系统管理进程也是一样的操作系统作为管理者是不需要直接和被管理者进程直接进行沟通的当一个进程出现时操作系统就立马对其进行描述之后对该进程的管理实际上就是对其描述信息的管理。进程信息被放在一个叫做进程控制块的数据结构中可以理解为进程属性的集合称之为PCBprocess control block。操作系统将每一个进程都进行描述形成了一个个的进程控制块PCB并将这些PCB以双链表的形式组织起来。 这样一来操作系统只要拿到这个双链表的头指针便可以访问到所有的PCB。此后操作系统对各个进程的管理就变成了对这条双链表的一系列操作。 下面我们看一个例子 创建一个进程实际上就是先将该进程的代码和数据加载到内存紧接着操作系统对该进程进行描述形成对应的PCB并将这个PCB插入到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效。总的来说操作系统对进程的管理实际上就变成了对该双链表的增、删、查、改等操作。 【Linux中的PCB————task_struct】 进程控制块PCB是描述进程的在C当中我们称之为面向对象类而在C语言当中我们称之为结构体既然Linux操作系统是用C语言进行编写的那么Linux当中的进程控制块必定是用结构体来实现的。 PCB实际上是对进程控制块的统称在Linux中描述进程的结构体叫做task_struct。task_struct是Linux内核的一种数据结构它会被装载到RAM内存里并且包含进程的信息。 【task_struct的内容分类】 task_struct就是Linux当中的进程控制块task_struct当中主要包含以下信息 标示符 描述本进程的唯一标示符用来区别其他进程PID和PPID就是一种标识符。状态 任务状态退出代码退出信号等。优先级 相对于其他进程的优先级。程序计数器(pc) 程序中即将被执行的下一条指令的地址。内存指针 包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针。上下文数据 进程执行时处理器的寄存器中的数据。I/O状态信息 包括显示的I/O请求分配给进程的I/O设备和被进程使用的文件列表。记账信息 可能包括处理器时间总和使用的时钟总和时间限制记账号等。其他信息。 对应如下 struct task_struct
{volatile long state; //说明了该进程是否可以执行,还是可中断等信息unsigned long flags; //Flage 是进程号,在调用fork()时给出int sigpending; //进程上是否有待处理的信号mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同//0-0xBFFFFFFF for user-thead//0-0xFFFFFFFF for kernel-thread//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度volatile long need_resched;int lock_depth; //锁深度long nice; //进程的基本时间片//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHERunsigned long policy;struct mm_struct* mm; //进程内存管理信息int processor;//若进程不在任何CPU上运行, cpus_runnable 的值是0否则是1 这个值在运行队列被锁时更新unsigned long cpus_runnable, cpus_allowed;struct list_head run_list; //指向运行队列的指针unsigned long sleep_time; //进程的睡眠时间//用于将系统中所有的进程连成一个双向循环链表, 其根是init_taskstruct task_struct* next_task, * prev_task;struct mm_struct* active_mm;struct list_head local_pages; //指向本地页面 unsigned int allocation_order, nr_local_pages;struct linux_binfmt* binfmt; //进程所运行的可执行文件的格式int exit_code, exit_signal;int pdeath_signal; //父进程终止时向子进程发送的信号unsigned long personality;//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序int did_exec : 1;pid_t pid; //进程标识符,用来代表一个进程pid_t pgrp; //进程组标识,表示进程所属的进程组pid_t tty_old_pgrp; //进程控制终端所在的组标识pid_t session; //进程的会话标识pid_t tgid;int leader; //表示进程是否为会话主管struct task_struct* p_opptr, * p_pptr, * p_cptr, * p_ysptr, * p_osptr;struct list_head thread_group; //线程链表struct task_struct* pidhash_next; //用于将进程链入HASH表struct task_struct** pidhash_pprev;wait_queue_head_t wait_chldexit; //供wait4()使用struct completion* vfork_done; //供vfork() 使用unsigned long rt_priority; //实时优先级用它计算实时进程调度时的weight值//it_real_valueit_real_incr用于REAL定时器单位为jiffies, 系统根据it_real_value//设置定时器的第一个终止时间. 在定时器到期时向进程发送SIGALRM信号同时根据//it_real_incr重置终止时间it_prof_valueit_prof_incr用于Profile定时器单位为jiffies。//当进程运行时不管在何种状态下每个tick都使it_prof_value值减一当减到0时向进程发送//信号SIGPROF并根据it_prof_incr重置时间.//it_virt_valueit_virt_value用于Virtual定时器单位为jiffies。当进程运行时不管在何种//状态下每个tick都使it_virt_value值减一当减到0时向进程发送信号SIGVTALRM根据//it_virt_incr重置初值。unsigned long it_real_value, it_prof_value, it_virt_value;unsigned long it_real_incr, it_prof_incr, it_virt_value;struct timer_list real_timer; //指向实时定时器的指针struct tms times; //记录进程消耗的时间unsigned long start_time; //进程创建的时间//记录进程在每个CPU上所消耗的用户态时间和核心态时间long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];//内存缺页和交换信息://min_flt, maj_flt累计进程的次缺页数Copy on Write页和匿名页和主缺页数从映射文件或交换//设备读入的页面数 nswap记录进程累计换出的页面数即写到交换设备上的页面数。//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数主缺页数和换出页面数。//在父进程回收终止的子进程时父进程会将子进程的这些信息累计到自己结构的这些域中unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;int swappable : 1; //表示进程的虚拟地址空间是否允许换出//进程认证信息//uid,gid为运行该进程的用户的用户标识符和组标识符通常是进程创建者的uidgid//euidegid为有效uid,gid//fsuidfsgid为文件系统uid,gid这两个ID号通常与有效uid,gid相等在检查对于文件//系统的访问权限时使用他们。//suidsgid为备份uid,giduid_t uid, euid, suid, fsuid;gid_t gid, egid, sgid, fsgid;int ngroups; //记录进程在多少个用户组中gid_t groups[NGROUPS]; //记录进程所在的组//进程的权能分别是有效位集合继承位集合允许位集合kernel_cap_t cap_effective, cap_inheritable, cap_permitted;int keep_capabilities : 1;struct user_struct* user;struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息unsigned short used_math; //是否使用FPUchar comm[16]; //进程正在运行的可执行文件名//文件系统信息int link_count, total_link_count;//NULL if no tty 进程所在的控制终端如果不需要控制终端则该指针为空struct tty_struct* tty;unsigned int locks;//进程间通信信息struct sem_undo* semundo; //进程在信号灯上的所有undo操作struct sem_queue* semsleeping; //当进程因为信号灯操作而挂起时他在该队列中记录等待的操作//进程的CPU状态切换时要保存到停止进程的task_struct中struct thread_struct thread;//文件系统信息struct fs_struct* fs;//打开文件信息struct files_struct* files;//信号处理函数spinlock_t sigmask_lock;struct signal_struct* sig; //信号处理函数sigset_t blocked; //进程当前要阻塞的信号每个信号对应一位struct sigpending pending; //进程上是否有待处理的信号unsigned long sas_ss_sp;size_t sas_ss_size;int (*notifier)(void* priv);void* notifier_data;sigset_t* notifier_mask;u32 parent_exec_id;u32 self_exec_id;spinlock_t alloc_lock;void* journal_info;
}; 【task_struct中的PID】 PID是进程标识符Process IDentifier的缩写是Linux和其他类Unix操作系统中用来唯一标识一个正在运行的进程的数字。每个进程都有一个唯一的PID该PID是由操作系统分配的并且在系统范围内保持唯一性。 PID的主要作用包括 唯一标识进程PID能够在系统范围内唯一标识一个进程即使在多个用户空间或不同的终端中。 进程管理通过PID系统管理员可以轻松地查找、监视、控制和终止特定的进程。 进程通信某些进程间通信IPC机制可能需要使用PID来标识目标进程以便发送消息或执行其他操作。 错误排查在排查系统问题时PID可以帮助定位特定进程可能引发的错误或异常情况。 总之PID是Linux和类Unix系统中用于唯一标识正在运行的进程的数字标识符是进程管理和通信的重要组成部分。我们知道PID是由操作系统在创建一个进程时为该进程分配的一个具有唯一性的进程标识符通常为一个整数。通过PID我们不仅可以便于对进程进行管理同时操作系统还可以通过这个PID来判断该进程是否具有执行权限一句话就是PID存在的目的是为了便于操作系统对进程进行管理 我们了解到进程在创建时操作系统会为其分配唯一的进程标识符PID那么我们如何得到进程的标识符呢接下来让我们认识两个系统调用函数getpid() 和 getppid() 【获取进程的PID和PPID】 首先我们要知道进程的PID和PPID本质上是进程的编号存在于进程的task_struct中每个进程都有自己的PID和PPID其中PID是唯一的而进程的PPID不一定唯一它们存在的根本目的是方便操作系统对进程进行管理。 进程idPID使用系统调用函数getpid可以获取。父进程idPPID使用系统调用函数getppid可以获取。 我们可以通过一段代码来进行测试代码如下 当运行该代码生成的可执行程序后便可循环打印该进程的PID和PPID。 我们可以通过ps命令查看该进程的信息即可发现通过ps命令得到的进程的PID和PPID与使用系统调用函数getpid和getppid所获取的值相同。 所以通过这两个函数我们就可以获得进程的PID和PPID。 二、【了解进程操作】 2.1、【查看进程】 我们前面知道了进程是什么以及我们该如何描述进程现在让我们看看进程是什么样子的。 1、通过系统目录查看 在根目录下有一个名为proc的系统文件夹。 文件夹当中包含大量进程信息其中有些子目录的目录名为数字。 这些数字其实是某一进程的PID对应文件夹当中记录着对应进程的各种信息。我们若想查看PID为1的进程的进程信息则查看名字为1的文件夹即可。 2、通过ps命令查看 单独使用ps命令会显示所有进程信息。 ps命令与grep命令搭配使用即可只显示某一进程的信息。 ps aux | head -2 ps aux | grep proc | grep -v grepps 命令的选项用于指定输出的格式和显示的内容。下面是一些常用的 ps 命令选项及其含义 -e显示所有进程等同于 -A。-f显示详细的进程信息包括进程的 UID、PID、PPID、C、STIME、TTY、TIME 和 CMD。-u以用户格式显示进程信息。-a显示终端上的所有进程包括其他用户的进程。 -x显示没有控制终端的进程。 j 以用户友好的格式显示进程信息包括进程的 PID进程 ID、PPID父进程 ID、PGID进程组 ID、SID会话 ID、UID用户 ID、STIME启动时间、TTY控制终端、TIMECPU 时间、CMD命令等。 -ww使用最宽的输出格式。aux同时列出所有的进程包括其他用户的进程并且显示详细信息。ajx 会列出所有进程的详细信息并以用户友好的格式显示。 3、通过top命令查看进程 top命令是一个非常有用的Linux系统监视工具可以显示系统中正在运行的进程以及相关的系统资源使用情况。以下是关于top命令的详细解释 启动 top 命令 在终端中输入 top 并按下回车键即可启动 top 命令。默认情况下top 将会以交互方式显示当前系统的运行状况最后使用ctrlc退出即可。 交互式界面top 命令以一个交互式界面展示系统资源的使用情况。在默认模式下它会按照 CPU 使用率排序显示进程列表。 实时更新top 命令会持续更新显示系统资源使用情况和进程信息。默认情况下它每隔 3 秒钟刷新一次。 显示信息 top 命令默认会显示如下信息 系统整体信息包括系统时间、运行时间、登录用户数量、系统负载等。进程信息包括进程 ID、用户、优先级、CPU 占用率、内存占用等。CPU 使用情况包括用户态、系统态、空闲等。内存使用情况包括总内存、已用内存、空闲内存等。交换空间使用情况包括交换总量、已用交换、空闲交换等。 交互命令 top 命令支持一些交互命令可以在其运行时进行操作。 h显示帮助信息列出可用的交互命令。 k结束一个进程。 q退出 top 命令。 r改变进程的优先级。 Space切换 CPU 时间的显示单位。 1显示多核 CPU 每个核的详细信息。参数设置 你可以使用一些参数来定制 top 命令的行为。例如top -n 5 将只显示前 5 次更新的信息。 2.2、【fork函数】 fork是一个系统调用级别的函数可以使用指令man fork 即可得到fork函数的函数接口的函数的使用方法。 接下来我们先了解一下什么是系统调用函数 系统调用函数是操作系统提供给用户程序或应用程序的一组接口通过这些接口用户程序可以请求操作系统执行特定的操作如文件操作、进程管理、网络通信等。系统调用函数允许用户程序访问操作系统的底层功能以完成对硬件资源的管理和控制。 系统调用函数与一般的函数调用有所不同。一般的函数调用是在用户程序内部进行的而系统调用函数是用户程序与操作系统之间的通信方式。当用户程序调用系统调用函数时会触发一个特殊的处理机制将控制权转移给操作系统内核执行相应的操作然后将结果返回给用户程序。 系统调用函数通常是由操作系统提供的库函数封装的以便用户程序更方便地调用。这些函数通常包含在标准库中例如在 C 语言中可以通过 unistd.h 头文件来访问系统调用函数。 常见的系统调用函数包括 fork()、exec()、open()、read()、write() 等它们提供了对文件系统、进程管理、内存管理、网络通信等底层功能的访问。系统调用函数是编写操作系统相关程序和系统编程的重要工具也是操作系统与用户程序之间的桥梁。 1、使用fork函数创建子进程 fork是一个系统调用级别的函数其功能就是创建一个子进程fork函数从已存在进程中创建一个新进程。新进程为子进程而原进程为父进程使用时注意包头文件unistd.h。 若是代码当中没有fork函数我们都知道代码的运行结果就是循环打印该进程的PID和PPID。而加入了fork函数后代码运行结果如下 运行结果是循环打印两行数据第一行数据是该进程的PID和PPID第二行数据是代码中fork函数创建的子进程的PID和PPID。我们可以发现fork函数创建的进程的PPID就是proc进程的PID也就是说proc进程与fork函数创建的进程之间是父子关系。 我们要知道每出现一个进程操作系统就会为其创建PCBfork函数创建的进程也不例外。那么fork函数创建的子进程PCB该如何创建呢 实际上操作系统会将其父进程PCB当作模板创建其子进程的PCB创建完成以后将其组织在一起方便管理。 我们知道加载到内存当中的代码和数据是属于父进程的那么fork函数创建的子进程的代码和数据又从何而来呢 我们先看看以下代码的运行结果 运行结果 实际上使用fork函数创建子进程进程从 fork() 返回的地方【return】开始执行而父进程则继续执行它的代码。这意味着在 fork() 调用之后父进程和子进程会并行执行。而在fork函数被调用之前的代码被父进程执行而fork函数之后的代码则默认情况下父子进程都可以执行。需要注意的是父子进程虽然代码共享但是父子进程的数据各自开辟空间采用写时拷贝。 注意 使用fork函数创建子进程后就有了两个进程这两个进程被操作系统调度的顺序是不确定的这取决于操作系统调度算法的具体实现。 2、使用if进行分流 上面说到fork函数创建出来的子进程与其父进程共同使用一份代码但我们如果真的让父子进程做相同的事情那么创建子进程就没有什么意义了。 实际上在fork之后我们通常使用if语句进行分流即让父进程和子进程做不同的事在此之前我们先来看看fork函数的返回值。 fork函数的返回值1、如果子进程创建成功在父进程中返回子进程的PID而在子进程中返回0。2、如果子进程创建失败则在父进程中返回 -1。 既然父进程和子进程获取到fork函数的返回值不同那么我们就可以据此来让父子进程执行不同的代码从而做不同的事。例如以下代码 fork创建出子进程后子进程会进入到 if 语句的循环打印当中而父进程会进入到 else if 语句的循环打印当中我们就会看到下面的场景。 这里我们留意下面这样两个问题后续进行解答 1、为什么fork函数会有两个返回值呢是如何做到的 2、我们定义的id变量是如何做到存储两个值的 三、【进程状态】 1、【进程的6种状态】 一个进程从创建而产生至撤销而消亡的整个生命期间有时占有处理器执行有时虽可运行但分不到处理器有时虽有空闲处理器但因等待某个时间的发生而无法执行这一切都说明进程和程序不相同进程是活动的且有状态变化的于是就有了进程状态这一概念。 操作系统的进程的状态有 创建就绪运行阻塞挂起终止。 各状态反映到PCB中其实本质就是整型变量status 创建状态New 新创建的进程正在等待分配资源或初始化。 就绪状态Ready 进程已经准备好运行但由于CPU资源有限或者其他进程正在执行因此暂时无法执行。进程在就绪队列中等待CPU时间片。 运行状态Running 每一个CPU在系统层面都会维护一个运行队列运行状态简单的理解就是PCB在运行队列中排队或者正在被cpu执行。也就是说处于运行队列中的进程就是运行状态。这时进程是万事俱备随时可以被调度。 操作系统中会存在非常多的队列运行队列阻塞队列以及等待硬件的设备等待队列等 并且所有系统内的进程是用双链表链接起来的。 阻塞状态Blocked 进程(进程 PCB 可执行程序(狭义上这么说))排队这件事是进程所对应的PCB来排队的。进程在排队的时候一定是在等待某种资源一般是系统资源比如键盘一些外部设备。而当我们的进程需要访问的资源没有准备好时处于运行状态的进程正在等待系统资源时我们称该进程处于阻塞状态。 对于以下代码 int main()
{int a;scanf(%d, a);printf(%d\n, a);return 0;
} 当程序走到了scanf 时那么程序就会卡住等待从键盘上获取资源。这时候操作系统会将该进程的pcb中的某个节点 连接到 对应的阻塞队列上去同时该进程已经不在运行队列了那么该进程的状态要从运行状态改为阻塞状态 当我们的进程正在等待软硬件资源的时候资源如果没有就绪我们进程PCB 只能将自己设置为阻塞状态并将自己的PCB连接到该资源提供的等待队列。 我们称进程从运行队列变化到阻塞队列导致其状态从运行状态变化到阻塞状态的这一过程称为状态变化其本质其实是改变进程PCB中的status整形变量以及将其PCB链入到不同队列。 当一个进程阻塞了我们会看到什么现象为什么 1.进程卡住了。 2.pcb没有在运行队列中状态不是running,CPU不调度你的进程了。 挂起状态suspend 挂起状态当进程处于阻塞状态时恰逢操作系统内存吃紧会将进程的数据置换到外设从而缓解内存此时该进程就处于挂起状态。 终止状态Terminated 进程执行结束释放了占用的资源并等待被操作系统回收。 总结 这些状态通常由操作系统的调度程序和内核来管理和维护。进程在不同状态之间转换的过程由操作系统的调度算法控制以实现对进程的合理调度和资源管理。 2、【Linux中的进程状态】 那么在Linux种进程的状态具体是怎样的呢下面是Linux中的几种状态: 这里我们具体谈一下Linux操作系统中的进程状态Linux操作系统的源代码当中对于进程状态有如下定义 /*
* 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.
*/
static const char *task_state_array[] {R (running), /* 0*/S (sleeping), /* 1*/D (disk sleep), /* 2*/T (stopped), /* 4*/T (tracing stop), /* 8*/Z (zombie), /* 16*/X (dead) /* 32*/
};小贴士 进程的当前状态是保存到自己的进程控制块task_struct当中的在Linux操作系统当中也就是保存在task_struct当中的。 在Linux操作系统当中我们可以通过 ps aux 命令进行查看。 也可以使用 ps axj 命令查看进程的状态。 运行状态-R 一个进程处于运行状态running并不意味着进程一定处于运行当中运行状态表明一个进程要么在运行中要么在运行队列里。也就是说可以同时存在多个R状态的进程。 小贴士 所有处于运行状态即可被调度的进程都被放到运行队列当中当操作系统需要切换进程运行时就直接在运行队列中选取进程运行。 浅度睡眠状态-S 一个进程处于浅度睡眠状态sleeping意味着该进程正在等待某件事情的完成这里也可以是等待外设资源处于浅度睡眠状态的进程随时可以被唤醒也可以被杀掉这里的睡眠有时候也可叫做可中断睡眠interruptible sleep本质上也是一种阻塞状态。 看下面的代码 代码当中调用sleep函数进行休眠100秒在这期间我们若是查看该进程的状态则会看到该进程处于浅度睡眠状态。 而处于浅度睡眠状态的进程是可以被杀掉的我们可以使用kill命令将该进程杀掉。 深度睡眠状态-D 一个进程处于深度睡眠状态disk sleep表示该进程不会被杀掉即便是操作系统也不行只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态uninterruptible sleep处于这个状态的进程通常会等待IO的结束本质上也是一种阻塞状态。 举个例子 ——在系统有一个进程叫“小张”磁盘有一个东西主要进程数据的存储叫“小陈”。 “小张”要把数据存放到磁盘中拜托“小陈”来存由于磁盘中的东西较多“小陈”要找一段时间而在这个时间段系统中的正在执行的进程越来越多最后操作系统看见“小张”“占着茅坑不拉屎”就把“小张”给踢出去了之后”小陈“存放数据失败了找”小张“闻着数据是删掉还是再存放一次然而”小张“已经被操作系统干掉了”小陈“得不到回响不知道怎么办。 为了防止这个情况的发生操作系统就搞了个D状态。 这种状态(D)的进程杀不死并且我们几乎在进程列表里没有机会看到该状态。 暂停状态-T 在Linux当中我们可以通过发送SIGSTOP信号使进程进入暂停状态stopped发送SIGCONT信号可以让处于暂停状态的进程继续运行。 例如我们对一个进程发送SIGSTOP信号该进程就进入到了暂停状态。 我们再对该进程发送SIGCONT信号该进程就继续运行了。 小贴士 使用kill命令可以列出当前系统所支持的信号集。 kill命令及常用信号介绍 kill 命令是用于向指定进程发送信号的工具。除了发送终止信号之外还可以发送其他一些信号来控制进程的行为其中包括暂停信号、重新运行信号、强制终止信号和终止信号。以下是关于这些信号的介绍 暂停信号SIGSTOP 信号编号19作用暂停挂起目标进程的执行使其停止运行。例子可以使用 kill -SIGSTOP 进程ID 或 kill -19 进程ID 命令来发送暂停信号。 重新运行信号SIGCONT 信号编号18作用恢复被暂停的进程的执行使其继续运行。例子可以使用 kill -SIGCONT 进程ID 或 kill -18 进程ID 命令来发送重新运行信号。 强制终止信号SIGKILL 信号编号9作用强制终止目标进程的执行立即结束进程的运行不给进程执行清理操作的机会。例子可以使用 kill -SIGKILL 进程ID 或 kill -9 进程ID 命令来发送强制终止信号。 终止信号SIGTERM 信号编号15作用通知目标进程正常退出允许进程执行清理操作后退出。例子可以使用 kill -SIGTERM 进程ID 或 kill -15 进程ID 命令来发送终止信号。 僵尸状态-Z 当一个进程将要退出的时候在系统层面该进程曾经申请的资源并不是立即被释放而是要暂时存储一段时间以供操作系统或是其父进程进行读取而每个进程退出时都会生成自己的推出信息如果退出信息一直未被读取则相关数据是不会被释放掉的一个进程若是正在等待其退出信息被读取那么我们称该进程处于僵尸状态zombie。 首先僵尸状态的存在是必要的因为进程被创建的目的就是完成某项任务那么当任务完成的时候调用方是应该知道任务的完成情况的所以必须存在僵尸状态使得调用方得知任务的完成情况以便进行相应的后续操作。例如我们写代码时都在主函数最后返回0。 实际上这个0就是返回给操作系统的告诉操作系统代码顺利执行结束。在Linux操作系统当中我们可以通过使用echo $?命令获取最近一次进程退出时的退出码。 小贴士 进程退出的信息例如退出码是暂时被保存在其进程控制块当中的在Linux操作系统中也就是保存在该进程的task_struct当中。 死亡状态-X 死亡状态只是一个返回状态当一个进程的退出信息被读取后该进程所申请的资源就会立即被释放该进程也就不存在了所以不会在任务列表当中看到死亡状态dead。 3、【父子进程、僵尸进程和孤儿进程】 【父子进程总结】 前面我们已经对父子进程进行了概述接下来我们总结一下父子进程的关系 父进程在Linux中父进程是生成其他进程的进程。通常情况下init进程PID为1是所有其他进程的祖先它负责系统的初始化和进程的管理。 子进程在Linux中子进程是由父进程生成的进程。当一个进程调用fork()系统调用时操作系统会创建一个新的进程子进程并将父进程的所有资源复制给子进程。子进程会继承父进程的文件描述符、信号处理方式等属性并与父进程共享代码段、数据段和堆栈但拥有独立的地址空间。 现在来详细解释一下0号、1号、2号进程及其父子关系 0号进程通常情况下0号进程指的是内核线程或系统启动时的第一个进程。在Linux中0号进程可能是内核线程如kthreadd或者是用于特定任务的内核进程。 1号进程在Linux中1号进程通常指的是init进程它是所有其他进程的祖先负责系统的初始化和进程的管理。 2号进程在Linux中2号进程通常指的是kthreadd进程它是内核线程创建进程负责创建和管理内核线程。 在创建子进程后父进程和子进程会继续并发执行它们之间的执行顺序取决于调度器的调度策略。 【僵尸进程】 前面说到一个进程若是正在等待其退出信息被读取那么我们称该进程处于僵尸状态。而处于僵尸状态的进程我们就称之为僵尸进程。 例如对于以下代码fork函数创建的子进程在打印5次信息后会退出而父进程会一直打印信息。也就是说子进程退出了父进程还在运行但父进程没有读取子进程的退出信息那么此时子进程就进入了僵尸状态。 #include stdio.h
#include stdlib.h
#include unistd.h
int main()
{printf(I am running...\n);pid_t id fork();if(id 0){ //childint count 5;while(count){printf(I am child...PID:%d, PPID:%d, count:%d\n, getpid(), getppid(), count);sleep(1);count--;}printf(child quit...\n);exit(1);}else if(id 0){ //fatherwhile(1){printf(I am father...PID:%d, PPID:%d\n, getpid(), getppid());sleep(1);}}else{ //fork error}return 0;
} 运行该代码后我们可以通过以下监控脚本每隔一秒对该进程的信息进行检测。 while :; do ps axj | head -1 ps axj | grep proc | grep -v grep;echo ######################;sleep 1;done检测后即可发现当子进程退出后子进程的状态就变成了僵尸状态。 通过监视我们可以看到子进程先退出后由于父进程还在一直运行无法对子进程进行回收这就导致子进程进入“僵死状态”即Z状态。虽然子进程已经退出但其进程ID和部分进程信息仍然保留在系统中可能导致进程表膨胀从而影响系统的正常运行。虽然僵尸进程本身不再消耗 CPU 资源或执行任何任务但其 PCB 仍然占用系统内存空间并需要操作系统来管理和维护。但其不再消耗 CPU 资源或执行任何任务当僵尸进程被回收时该进程的PCB及相关资源才会被彻底清理。 解决僵尸进程问题的一种常见方法是确保父进程及时对其子进程进行回收。这可以通过在父进程中捕获 SIGCHLD 信号并在信号处理函数中调用 wait() 或 waitpid() 来实现。此外Linux系统的init进程也会定期扫描并回收僵尸进程以确保系统的稳定性和性能。 僵尸进程的危害 僵尸进程的退出状态必须一直维持下去因为它要告诉其父进程相应的退出信息。可是父进程一直不读取那么子进程也就一直处于僵尸状态。僵尸进程的退出信息被保存在task_struct(PCB)中僵尸状态一直不退出那么PCB就一直需要进行维护。若是一个父进程创建了很多子进程但都不进行回收那么就会造成资源浪费因为数据结构对象本身就要占用内存。僵尸进程申请的资源无法进行回收那么僵尸进程越多实际可用的资源就越少也就是说僵尸进程会导致内存泄漏。 【孤儿进程】 在Linux当中的进程关系大多数是父子关系若子进程先退出而父进程没有对子进程的退出信息进行读取那么我们称该进程为僵尸进程。但若是父进程先退出那么将来子进程进入僵尸状态时就没有父进程对其进行处理此时该子进程就称之为孤儿进程。若是一直不处理孤儿进程的退出信息那么孤儿进程就会一直占用资源此时就会造成内存泄漏。因此当出现孤儿进程的时候孤儿进程会被1号init进程领养此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。 孤儿进程是指其父进程已经退出或者被终止但是该进程还在继续执行的情况下产生的进程。在Linux系统中孤儿进程会被init 进程即1号进程接管。init 进程会成为孤儿进程的新父进程并负责对其进行收养和管理确保进程能够正常执行。 孤儿进程的产生常见于以下情况 父进程意外终止但子进程仍在运行。父进程在子进程之前退出导致子进程成为孤儿进程。父进程忽略或者未能正确处理子进程的退出信号。 例如对于以下代码fork函数创建的子进程会一直打印信息而父进程在打印5次信息后会退出此时该子进程就变成了孤儿进程。 #include stdio.h
#include stdlib.h
#include unistd.h
int main()
{printf(I am running...\n);pid_t id fork();if(id 0){ //childint count 5;while(1){printf(I am child...PID:%d, PPID:%d\n, getpid(), getppid(), count);sleep(1);}}else if(id 0){ //fatherint count 5;while(count){printf(I am father...PID:%d, PPID:%d, count:%d\n, getpid(), getppid(), count);sleep(1);count--;}printf(father quit...\n);exit(0);}else{ //fork error}return 0;
} 观察代码运行结果在父进程未退出时子进程的PPID就是父进程的PID而当父进程退出后子进程的PPID就变成了1即子进程被1号进程领养了。 四、【进程优先级】 1、【基本概念】 正式开始之前我们先看看下面的两个问题 什么是优先级优先级实际上就是获取某种资源的先后顺序而进程优先级实际上就是进程获取CPU资源分配的先后顺序就是指进程的优先权priority优先权高的进程有优先执行的权力。 优先级存在的原因优先级存在的主要原因就是资源是有限的而存在进程优先级的主要原因就是CPU资源是有限的一个CPU一次只能跑一个进程而进程是可以有多个的所以需要存在进程优先级来确定进程获取CPU资源的先后顺序。 2、【查看优先级信息——PRI和NI】 在Linux或者Unix操作系统中用ps -l命令会类似输出以下几个内容 也可以使用ps -al命令查看该进程优先级的信息 列出的信息当中有几个重要的信息如下 UID代表执行者的身份。PID代表这个进程的代号。PPID代表这个进程是由哪个进程发展衍生而来的亦即父进程的代号。PRI代表这个进程可被执行的优先级其值越小越早被执行。NI代表这个进程的nice值。 我们知道UID是执行者的身份PID和PPID我们也已经介绍过。那么PRI和NI是什么呢它们是如何对进程的优先级造成影响的呢 我们先来了解一下两者的概念 在Linux中PRI代表进程的静态优先级Static PriorityNI代表进程的调度优先级Nice Value。这两个值是用来确定进程调度顺序的重要参数。 静态优先级PRI数值越小表示优先级越高。在进程的调度中静态优先级决定了进程在就绪队列中的顺序。进程的静态优先级可以通过nice命令调整。 调度优先级NI也称为Nice值它是用来调整进程的静态优先级的偏移量。Nice值的范围一般是-20到19数值越大表示优先级越低即进程更“nice”占用更少的CPU资源。可以通过nice和renice命令来调整进程的调度优先级。 这两个值共同决定了进程在CPU上执行的优先级。体现为PRI(new) PRI(old) NI 通过调整PRI和NI的值可以对进程的调度行为进行影响以满足不同的性能需求和系统资源分配策略。 总结 PRI代表进程的优先级priority通俗点说就是进程被CPU执行的先后顺序该值越小进程的优先级别越高。NI代表的是nice值其表示进程可被执行的优先级的修正数值。PRI值越小越快被执行当加入nice值后将会使得PRI变为PRI(new) PRI(old) NI。若NI值为负值那么该进程的PRI将变小即其优先级会变高。调整进程优先级在Linux下就是调整进程的nice值。NI的取值范围是-20至19一共40个级别。 注意 在Linux操作系统当中PRI(old)默认为80即PRI 80 NI。 3、【进程优先级的修改】 我们了解到PRI是是由操作系统分配给进程的初始优先级并且通常情况下是由系统管理员或具有特定权限的用户通过一些工具或命令来修改的。可见作为普通用户我们并没有权限直接对程序的PRI进行直接修改。 那么当我们需要更改进程优先级时又该如何做呢 通过top命令更改进程的nice值 前面我们知道top命令能够查看进程信息就相当于Windows操作系统中的任务管理器它能够动态实时的显示系统当中进程的资源占用情况。 使用top命令后按“r”键会要求你输入待调整nice值的进程的PID。 输入进程PID并回车后会要求你输入调整后的nice值。 输入nice值后按“q”即可退出如果我们这里输入的nice值为10那么此时我们再用ps命令查看进程的优先级信息即可发现进程的NI变成了10PRI变成了9080NI。 需要注意的是普通用户无法将NI值设置为负数即无法提升进程的优先级。如若是想将NI值调为负值也就是将进程的优先级调高需要使用sudo命令提升权限。 sudo nice -n -10 command [arguments]
//nice_value 是你想设置的 nice 值。
//command 是你想运行的命令。
//[arguments] 是该命令的参数如果有的话。可以看到我们使用sudo提权后我们成功将NI值设置为了负数成功提高了进程的优先级。 通过renice命令更改进程的nice值 使用renice命令后面跟上更改后的nice值和进程的PID即可。 之后我们再用ps命令查看进程的优先级信息也可以发现进程的NI变成了10PRI变成了9080NI。 注意 若是想使用renice命令将NI值调为负值也需要使用sudo命令提升权限。 renice nice_value -p pid
//nice_value 是你想设置的新 nice 值。
//-p 是一个选项用于指定后面跟的是进程 IDPID。
//pid 是你想修改 nice 值的进程的 ID。 4、【四个重要概念】 独立性: 多进程运行需要独享各种资源多进程运行期间互不干扰 并行: 多个进程在多个CPU下分别同时进行运行这称之为并行 竞争性 系统进程数目众多而CPU资源只有少量甚至1个所以进程之间是具有竞争属性的。为了高效完成任务更合理竞争相关资源便有了优先级。 并发 多个进程在一个CPU下采用进程切换的方式在一段时间之内让多个进程都得以推进称之为并发。 五、【环境变量】 1、【环境变量的介绍】 环境变量environment variables一般是指在操作系统中用来指定操作系统运行环境的一些参数。 例如我们编写的C/C代码在各个目标文件进行链接的时候从来不知道我们所链接的动静态库在哪里但是照样可以链接成功生成可执行程序原因就是有相关环境变量帮助编译器进行查找。 并且环境变量通常具有某些特殊用途并且在系统当中通常具有全局特性。 总结为一下几类 系统环境变量这些变量在整个系统范围内可用并影响所有用户和进程。例如PATH 变量定义了系统在哪些目录中查找可执行文件。 用户环境变量这些变量是针对特定用户的并且只影响该用户的会话。例如HOME 变量指定了用户的主目录路径。 临时环境变量这些变量通常由 shell 临时设置并在当前 shell 会话中有效。它们通常用于在特定操作中使用而不是永久性地影响系统或用户的配置。 永久环境变量这些变量在系统启动时由配置文件设置并且在整个系统的生命周期内有效。例如在 .bashrc 或 /etc/profile 中定义的变量。 常见环境变量 PATH 指定命令的搜索路径。HOME 指定用户的主工作目录即用户登录到Linux系统中的默认所处目录。SHELL 当前Shell它的值通常是/bin/bash。 查看环境变量的方法 我们可以通过echo命令来查看环境变量方式如下 echo $NAME //NAME为待查看的环境变量名称 2、【测试PATH】 大家有没有想过这样一个问题为什么执行ls命令的时候不用带./就可以执行而我们自己生成的可执行程序必须要在前面带上./才可以执行 容易理解的是要执行一个可执行程序必须要先找到它在哪里既然不带./就可以执行ls命令说明系统能够通过ls名称找到ls的位置而系统是无法找到我们自己的可执行程序的所以我们必须带上./以此告诉系统该可执行程序位于当前目录下。 而系统就是通过环境变量PATH来找到ls命令的查看环境变量PATH我们可以看到如下内容 可以看到环境变量PATH当中有多条路径这些路径由冒号隔开当你使用ls命令时系统就会查看环境变量PATH然后默认从左到右依次在各个路径当中进行查找。 而ls命令实际就位于PATH当中的某一个路径下所以就算ls命令不带路径执行系统也是能够找到的。 通过上面我们知道原来ls并不是不需要被找到而是ls存在的位置被存放在环境变量中了当使用ls指令时如果不加上其存在的位置会默认从环境变量中的各路径中对ls进行查找。 那可不可以让我们自己的可执行程序也不用带路径就可以执行呢 当然可以下面给出两种方式 方式一将可执行程序拷贝到环境变量PATH的某一路径下。 既然在未指定路径的情况下系统会根据环境变量PATH当中的路径进行查找那我们就可以将我们的可执行程序拷贝到PATH的某一路径下此后我们的可执行程序不带路径系统也可以找到了。 sudo cp proc /usr/bin方式二将可执行程序所在的目录导入到环境变量PATH当中。 将可执行程序所在的目录导入到环境变量PATH当中这样一来没有指定路径时系统就会来到该目录下进行查找了。 export PATH$PATH:/home/xzc/work/linux/proc3、【测试HOME】 任何一个用户在运行系统登录时都有自己的主工作目录家目录环境变量HOME当中即保存的该用户的主工作目录。 4、【测试SHELL】 我们在Linux操作系统当中所敲的各种命令实际上需要由命令行解释器进行解释而在Linux当中有许多种命令行解释器例如bash、sh我们可以通过查看环境变量SHELL来知道自己当前所用的命令行解释器的种类。 5、【环境变量相关命令】 echo显示某个环境变量的值。 export设置一个新的环境变量。 env显示所有的环境变量。 set显示本地定义的shell变量和环境变量。 unset清除环境变量。 永久性配置要永久性地配置环境变量可以将设置添加到用户的配置文件中例如 .bashrc 或 .bash_profile。 6、【环境变量的组织方式】 在系统当中环境变量的组织方式如下 每个程序都会收到一张环境变量表环境表是一个字符指针数组每个指针指向一个以’\0’结尾的环境字符串最后一个字符指针为空。 7、【获取环境变量】 【通过命令行参数获取环境变量】 你知道main函数其实是有参数的吗 main函数其实有三个参数只是我们平时基本不用它们所以一般情况下都没有写出来。 我们可以在Windows下的编译器进行验证当我们调试代码的时候若是一直使用逐步调试那么最终会来到调用main函数的地方。 在这里我们可以看到调用main函数时给main函数传递了三个参数。 我们先来说说main函数的前两个参数。 在Linux操作系统下编写以下代码生成可执行程序并运行。 运行结果如下 现在我们来说说main函数的前两个参数main函数的第二个参数是一个字符指针数组数组当中的第一个字符指针存储的是可执行程序的位置其余字符指针存储的是所给的若干选项最后一个字符指针为空而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。 下面我们可以尝试编写一个简单的代码该代码运行起来后会根据你所给选项给出不同的提示语句。 #include stdio.h
#include string.h
int main(int argc, char *argv[], char* envp[])
{if(argc 1){if(strcmp(argv[1], -a) 0){printf(you used -a option...\n);}else if(strcmp(argv[1], -b) 0){printf(you used -b option...\n);}else{printf(you used unrecognizable option...\n);}}else{printf(you did not use any option...\n);}return 0;
}代码运行结果如下 现在我们来说说main函数的第三个参数。 main函数的第三个参数接收的实际上就是环境变量表我们可以通过main函数的第三个参数来获取系统的环境变量。 例如编写以下代码生成可执行程序并运行。 运行结果就是各个环境变量的值 总结 int main(int argc, char *argv[], char *env[]) 是C/C语言中 main 函数的标准声明形式它的参数含义如下 argc表示命令行参数的数量argument count即程序运行时传递给程序的参数的个数。这个参数至少为1因为程序名本身也算一个参数。 argv是一个指向字符串数组的指针argument vector其中每个字符串都是一个命令行参数。argv[0] 存储的是程序的名称而 argv[1] 到 argv[argc-1] 存储的是传递给程序的命令行参数。 env是一个指向字符串数组的指针environment其中每个字符串都是一个环境变量的定义。每个环境变量都以形如 “NAMEvalue” 的格式存储。数组的最后一个元素通常是一个空指针用于指示环境变量列表的结束。 通过这些参数程序可以获取到命令行传递的参数和环境变量的值从而进行相应的处理。 【通过第三方变量environ获取】 除了使用main函数的第三个参数来获取环境变量以外我们还可以通过第三方变量environ来获取。 #include stdio.h
int main(int argc, char *argv[])
{
// environ是一个外部声明它声明了一个指向环境变量的指针数组的全局变量 environ。
// environ 指针数组就是指向这些储存环境变量字符串数组的指针数组extern char **environ;int i 0;for(; environ[i]; i){printf(%s\n, environ[i]);}return 0;
} 运行该代码生成的可执行程序我们同样可以获得环境变量的值 注意 libc中定义的全局变量environ指向环境变量表environ没有包含在任何头文件中所以在使用时要用extern进行声明。 【通过系统调用获取环境变量】 除了通过main函数的第三个参数和第三方变量environ来获取环境变量外我们还可以通过系统调用getenv函数来获取环境变量getenv 函数是一个C标准库函数用于获取指定名称的环境变量的值。其原型通常在 stdlib.h 头文件中声明 char *getenv(const char *name);该函数接受一个参数 name表示要获取的环境变量的名称返回一个指向该环境变量值的指针。如果指定名称的环境变量存在则返回该环境变量的值如果不存在则返回空指针NULL。 以下是一个简单的示例演示了如何使用 getenv 函数来获取指定环境变量的值 #include stdio.h
#include stdlib.hint main() {// 获取名为 PATH 的环境变量的值char *path_value getenv(PATH);if (path_value ! NULL) {printf(PATH环境变量的值%s\n, path_value);} else {printf(未找到PATH环境变量\n);}return 0;
} 在这个示例中程序首先调用 getenv(PATH) 来获取名为 “PATH” 的环境变量的值并将其存储在 path_value 变量中。然后程序检查 path_value 是否为NULL如果不是NULL则输出该环境变量的值如果是NULL则输出未找到该环境变量的消息。 再看一个例子使用getenv函数获取环境变量PATH的值 运行结果 注意环境变量具有全局性子进程会继承父进程的环境变量。 六、【进程地址空间】 1、【引入】 下面这张空间布局图相信大家都见过也叫做程序地址空间 在Linux操作系统中我们可以通过以下代码对该布局图进行验证 运行结果如下与布局图所示是吻合的 实际上我们在语言层面上打印出来的地址都不是物理地址而是虚拟地址。物理地址用户一概是看不到的是由操作系统统一进行管理的也就是说我们上面的程序地址空间并不是真正的物理内存所以其上的地址也不是真正的物理地址但是无论怎么说我们定义了变量必然就要开辟空间变量自然也是要存在物理空间也就是内存中的那么我们就会有一个疑问这些地址是虚拟地址指向的也是虚拟内存那为何我们对其解引用仍能获取到我们定义的变量呢 这是因为我们的操作系统为了简化对内存的管理以及提高内存利用率创建了程序地址空间这个虚拟空间使用虚拟空间中的虚拟地址经过某种转化使其映射到内存的物理空间上我们可以想象一下对于一段代码我们定义的各种变量是如何在内存上申请空间呢毕竟我们的内存又没有进行程序地址空间那样的分区所以实际我们每定义一个变量在内存是任意申请空间的这样对我们的内存来说实际上是十分混乱的并且对于程序代码来说因为有程序地址空间去映射所以定义的那些变量都会存在于对应的分区中所以程序地址空间存在的本质就是无论物理内存中的空间资源是如何申请利用的对于我们的每一份程序代码来说我们定义的任何变量开辟的任何空间消耗的都是程序地址空间中对应分区的空间资源。 注意 虚拟地址和物理地址之间的转化由操作系统完成。 再让我们先来看一段代码可以知道代码中使用if对父子进程进行分流并定义了一个全局变量g_value。 代码当中用fork函数创建了一个子进程其中让子进程相将全局变量g_val该从100改为200后打印而父进程先休眠3秒钟然后再打印全局变量的值。按道理来说子进程打印的全局变量的值为200而父进程是在子进程将全局变量改后再打印的全局变量那么也应该是200但是代码运行结果 可以看到父进程打印的全局变量g_val的值仍为之前的100更奇怪的是在父子进程中打印的全局变量g_val的地址是一样的也就是说父子进程在同一个地址处读出的值不同。 如果说我们是在同一个物理地址处获取的值那必定是相同的而现在在同一个地址处获取到的值却不同这只能说明我们打印出来的地址绝对不是物理地址 还是那句话我们在语言层面上打印出来的地址都不是物理地址而是虚拟地址。物理地址用户一概是看不到的是由操作系统统一进行管理的。 所以就算父子进程当中打印出来的全局变量的地址虚拟地址相同但是两个进程当中全局变量的值却是不同的这是因为经过某种转化以后父子进程虽然有相同的虚拟地址但是映射的是不同的物理空间。 2、【虚拟内存——进程地址空间的介绍】 实际上我们之前将那张布局图称为程序地址空间实际上是不准确的那张布局图实际上应该叫做进程地址空间进程地址空间本质上是内存中的一种内核数据结构在Linux当中进程地址空间具体由结构体mm_struct实现。 进程地址空间就类似于一把尺子尺子的刻度由0x00000000到0xffffffff尺子按照刻度被划分为各个区域例如代码区、堆区、栈区等。而在结构体mm_struct当中便记录了各个边界刻度例如代码区的开始刻度与结束刻度如下图所示 在结构体mm_struct当中各个边界刻度之间的每一个刻度都代表一个虚拟地址这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是由0x00000000到0xffffffff线性增长的所以虚拟地址又叫做线性地址。 那么问题来了虚拟内存是如何与物理内存联系起来的呢以及页表是什么东西 我们先来看看虚拟内存是如何与物理内存联系起来的呢 我们学过一种数据结构哈希表。而虚拟内存与物理内存之间的联系就相当于哈希映射即键值对的映射。 与之类似虚拟内存与物理内存之间的联系确实可以类比于哈希表的键值对映射。在虚拟内存系统中虚拟地址就像是哈希表的键而物理地址则是对应的值。操作系统通过页表这样的数据结构来实现这种映射关系。 当一个进程访问其虚拟地址时操作系统首先会将这个虚拟地址作为键进行哈希运算得到对应的哈希值。这个哈希值通常对应着页表中的一个索引位置。然后操作系统在页表中查找这个索引位置以确定该虚拟地址对应的物理地址。 如果在页表中找到了对应的物理地址那么就可以直接访问物理内存中的数据。如果没有找到则可能会触发缺页异常这时操作系统会根据某种页替换算法将一些不常用的页面换出到磁盘上然后将需要访问的页面从磁盘加载到物理内存中并更新页表的映射关系。 还有什么是页表呢让我们再深入了解一下页表 页表Page Table是操作系统中用于管理虚拟内存和物理内存映射关系的数据结构。在现代计算机系统中虚拟内存是指程序所见到的内存空间而物理内存是真正的计算机内存。 当程序在运行时它所使用的内存地址是虚拟地址而不是实际的物理地址。虚拟地址需要通过页表转换为物理地址才能在物理内存中找到相应的数据。 页表的主要作用包括 地址转换将程序的虚拟地址映射到物理内存中的实际地址。通过页表操作系统可以根据程序提供的虚拟地址找到相应的物理地址。 内存保护通过设置页表中的权限位例如读、写、执行权限可以对内存进行保护防止未经授权的访问。 内存管理页表可以跟踪每个页的使用情况以便进行页面置换和内存回收等管理操作。 内存分配当程序需要更多内存时操作系统可以根据页表信息动态分配新的物理页。 页表通常是一个由操作系统维护的数据结构存储在内存中。在进行地址转换时CPU会根据页表中的信息将虚拟地址转换为物理地址。 扩展知识 堆向上增长以及栈向下增长实际就是改变mm_struct当中堆和栈的边界刻度。 我们生成的可执行程序实际上也被分为了各个区域例如初始化区、未初始化区等。当该可执行程序运行起来时操作系统则将对应的数据加载到对应内存当中即可大大提高了操作系统的工作效率。而进行可执行程序的“分区”操作的实际上就算编译器所以说代码的优化级别实际上是编译器说了算。 我们来看一下具体的过程 每个进程被创建时其对应的进程控制块task_struct和进程地址空间mm_struct也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct因为task_struct当中有一个结构体指针存储的是mm_struct的地址。 例如父进程有自己的task_struct和mm_struct该父进程创建的子进程也会以父进程的task_struct和mm_struct为模板创建其自己的task_struct和mm_struct父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置如下图 而当子进程刚刚被创建时子进程和父进程的数据和代码是共享的即父子进程的代码和数据通过页表映射到物理内存的同一块空间。由于进程之间存在独立性父子进程虽然共享数据但是不能因为某一个进程需要修改数据而影响另一个进程所以当子进程需要修改数据时才将父进程的数据在内存当中拷贝一份然后再进行修改这里的拷贝采用的就是写时拷贝。 例如子进程需要将全局变量g_val改为200那么此时就在内存的某处存储g_val的新值并且改变子进程当中g_val的虚拟地址通过页表映射后得到的物理地址即可。 这种在需要进行数据修改时再进行拷贝的技术称为写时拷贝技术。 3、【进程空间存在的意义和几个重要的问题】 【进程空间存在的意义】 进程地址空间是指每个运行中的进程所拥有的虚拟内存空间包含了该进程运行所需的代码、数据以及堆栈等信息。进程地址空间的意义主要体现在以下几个方面 隔离性每个进程都拥有独立的地址空间使得各个进程之间的内存相互隔离互不干扰。这种隔离性可以防止进程之间的数据共享和相互干扰提高了系统的稳定性和安全性。 保护性进程地址空间可以通过设置权限位和访问控制来保护其中的数据防止未经授权的进程访问和修改。这种保护性可以有效地保护进程的私有数据和系统关键信息提高了系统的安全性。 共享性虽然进程地址空间是相互隔离的但系统可以通过内存映射等机制实现进程间的内存共享。这种共享性可以提高系统资源的利用效率加快进程间通信的速度促进进程间的协作与交互。 动态性进程地址空间的大小可以根据进程的需要动态调整使得进程能够灵活地管理和利用内存资源。这种动态性可以使系统更加高效地分配和利用内存提高了系统的性能和响应速度。 【解答几个重要的问题】 首先来看看我们之前遗留的问题 1、为什么fork函数会有两个返回值呢 再回答之前我们先来看看fork函数具体做了什么当进程调用 fork() 函数时实际上fork() 函数的代码会转移到操作系统内核中执行 。在内核中fork() 函数主要完成以下操作 创建新的进程控制块PCB内核会为新的子进程分配一个唯一的进程标识符PID并在内存中为其创建一个新的进程控制块PCB。这个 PCB 将包含子进程的运行状态、程序计数器、堆栈指针、文件描述符等信息。 复制父进程的地址空间以创建自己的地址空间在大多数情况下fork() 函数会创建子进程的完整副本包括代码段、数据段、堆栈等。这意味着子进程将会获得与父进程几乎完全相同的内存映像。只有子进程需要修改内存时才会进行实际的复制操作这一步通常通过 Copy-On-Write写时复制技术来实现。 将子进程的状态设置为就绪一旦子进程的地址空间准备好内核将其状态设置为就绪态以便在合适的时机可以被调度执行。 返回不同的值由于fork之后父子进程代码共享而fork函数内部的return语句也是代码所以父子进程都会执行所以在内核中fork() 函数会返回两次一次是在父进程的上下文中返回子进程的 PID另一次是在子进程的上下文中返回 0。这样父进程和子进程可以根据返回值来执行不同的代码路径。 在fork函数内部在执行 return pid 之前子进程就已经创建完成所以 return pid 实际也是父子进程的共享代码部分所以父进程会执行一次返回子进程的pid而子进程也会执行一次 return pid 返回进程是否创建完成的信息。 2、为什么父进程接收子进程的PID而子进程返回0或-1 父进程接收子进程的PID父进程在调用fork()函数后会得到子进程的PID作为返回值。通过这个PID父进程可以对子进程进行跟踪、管理和通信。例如父进程可能会使用子进程的PID来等待子进程的结束状态通过waitpid()函数或者向子进程发送信号通过kill()函数等。 子进程返回0或-1子进程在fork()函数返回时需要确定自己是父进程还是子进程。因此子进程通常会检查fork()的返回值来确定自己的身份。具体来说如果fork()返回0则表示当前进程是子进程。子进程可以通过这个返回值来区别自己和父进程并且通常会在这个基础上执行特定的任务或代码段。如果fork()返回-1则表示进程创建失败。通常这种情况会发生在系统资源不足或者其他错误发生时。子进程在这种情况下会立即退出或者采取相应的错误处理措施。 3、我们定义的id变量是如何做到存储两个值的 这里我们首先可以确定这里id的地址绝对不是物理地址而是虚拟地址因为物理地址是无论如何无法做到一个地址存放两个值原来子进程被创建以后以父进程的task_struct和mm_struct为模板创建自己的但是在物理内存上还是共享的父进程的代码和数据由于fork函数对于父子进程返回值不同所以子进程要改变自己对应的id的值来接收fork函数的返回值子进程就会通过写时拷贝在物理空间中创建一份自己的id并将页表中的虚拟地址与其映射最终实现一个变量存储两个值。 4、为什么数据要进行写时拷贝 进程具有独立性。多进程运行需要独享各种资源多进程运行期间互不干扰不能让子进程的修改影响到父进程。 5、为什么不在创建子进程的时候就进行数据的拷贝 子进程不一定会使用父进程的所有数据并且在子进程不对数据进行写入的情况下没有必要对数据进行拷贝我们应该按需分配在需要修改数据的时候再分配延时分配这样可以高效的使用内存空间。 6、代码会不会进行写时拷贝 90%的情况下是不会的但这并不代表代码不能进行写时拷贝例如在进行进程替换的时候则需要进行代码的写时拷贝。 7、为什么要有进程地址空间 有了进程地址空间后就不会有任何系统级别的越界问题存在了。例如进程1不会错误的访问到进程2的物理地址空间因为你对某一地址空间进行操作之前需要先通过页表映射到物理内存而页表只会映射属于你的物理内存。总的来说虚拟地址和页表的配合使用本质功能就是包含内存。 有了进程地址空间后每个进程都认为看得到都是相同的空间范围包括进程地址空间的构成和内部区域的划分顺序等都是相同的这样一来我们在编写程序的时候就只需关注虚拟地址而无需关注数据在物理内存当中实际的存储位置。 有了进程地址空间后每个进程都认为自己在独占内存这样能更好的完成进程的独立性以及合理使用内存空间当实际需要使用内存空间的时候再在内存进行开辟并能将进程调度与内存管理进行解耦或分离。 8、现阶段应该如何理解进程创建 一个进程的创建实际上伴随着其进程控制块task_struct、进程地址空间mm_struct以及页表的创建。 9、为什么子进程一开始不直接创建自己的物理内存空间而是进行写时复制呢 当一个子进程被创建时通常会通过复制父进程的地址空间来创建自己的地址空间。如果直接复制父进程的物理内存空间那么可能会浪费大量的内存资源特别是当子进程立即执行exec()系统调用来加载新的程序时因为此时父进程的内存内容对于子进程来说是无用的。 因此为了避免不必要的内存复制和浪费操作系统采用了写时复制技术。写时复制允许子进程与父进程共享相同的物理内存空间只有在子进程或父进程尝试修改内存中的数据时才会执行实际的内存复制操作将要修改的数据复制到子进程的独立内存空间中。这样可以节省内存空间并且减少了不必要的内存复制操作提高了系统的性能和效率。 总的来说写时复制技术使得子进程能够延迟对父进程内存空间的复制只在需要修改时才进行复制从而节省内存资源并提高系统的性能。 总结 总的来说进程地址空间的意义在于提供了一个独立、隔离、保护、共享和动态的内存空间为进程的正常运行和系统的稳定性、安全性提供了重要的基础。 七、【Linux系统中进程的调度】 相信我们们学完进程优先级后一定会好奇在Linux系统中进程如此繁多操作系统到底是如何对每个进程进行调度的呢Linux系统下的调度方式与一般的操作系统并不相同。 接下来我们先引出三个概念活动队列、过期队列和O(1)调度器。 【概念引入】 Linux活动队列运行队列 活动队列是Linux内核中用于存储正在运行和等待运行的进程的队列。活动队列中的进程是具有时间片的它们正在等待CPU执行。内核通过活动队列来决定下一个要执行的进程。 过期队列 过期队列是Linux内核中的一个数据结构用于存储已经用完时间片的进程。当进程的时间片用尽时它会被移动到过期队列中等待重新调度。在过期队列中的进程需要等待一个新的时间片以便重新执行。 O(1)调度器 O(1)调度器是Linux内核中一种优化的进程调度算法。它的设计目标是在常数时间内即O(1)时间复杂度完成进程调度而不受进程数量的影响。O(1)调度器使用了活动队列和过期队列以及一些其他数据结构以便快速地选择下一个要执行的进程。这种调度器的设计旨在提高系统的响应速度和性能。 总的来说Linux活动队列和过期队列是用于管理进程调度的数据结构而O(1)调度器是一种基于这些数据结构设计的高效调度算法它能够在常数时间内完成进程调度从而提高系统的性能和响应速度。 我们进程的都是普通的优先级前面说到nice值的取值范围是-20~19共40个级别依次对应queue当中普通优先级的下标100~139。 注意 实时优先级对应实时进程实时进程是指先将一个进程执行完毕再执行下一个进程现在基本不存在这种机器了所以对于queue当中下标为0~99的元素我们不关心。 【活动队列】 时间片还没有结束的所有进程都按照优先级放在活动队列当中其中nr_active代表总共有多少个运行状态的进程而queue[140]数组当中的一个元素就是一个进程队列相同优先级的进程按照FIFO规则进程排队调度。 过程如下 从0下标开始遍历queue[140]。找到第一个非空队列该队列必定为优先级最高的队列。拿到选中队列的第一个进程开始运行调度完成。接着拿到选中队列的第二个进程进行调度直到选中进程队列当中的所有进程都被调度。继续向后遍历queue[140]寻找下一个非空队列。 bitmap[5]bitmap[5]是一个无符号整型数组unsigned long用于表示进程队列中的进程是否在运行。数组的每个元素都是一个32位的整数这些整数的每一位对应着进程队列中的一个进程状态即是否在运行或已结束。queue数组当中一共有140个元素即140个优先级一共140个进程队列为了提高查找非空队列的效率就可以用5 × \times× 32个比特位表示队列是否为空。 假设我们有140个进程需要表示而一个32位整数可以表示32个进程状态。因此我们需要至少5个32位整数共160个比特位来表示这些进程。这就是为什么使用bitmap[5]数组。 在这个数组中每个比特位表示一个进程的状态通常用1来表示进程在运行队列中用0表示进程已经结束。这种方式可以有效地节省内存空间并且能够快速地进行进程状态的检查和修改。 使用位运算可以方便地对bitmap进行操作比如设置某个进程的状态、检查某个进程的状态等。这便是O(1)调度算法因为该算法是很小的常数级算法高效率且所需内存空间小因此它在操作系统中被广泛应用。 总结 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数不会随着进程增多而导致时间成本增加我们称之为进程调度的O(1)算法。 【过期队列】 过期队列和活动队列的结构相同。过期队列上放置的进程都是时间片耗尽的进程。当活动队列上的进程被处理完毕之后对过期队列的进程进行时间片重新计算。 【active指针和expired指针】 active指针永远指向活动队列。expired指针永远指向过期队列。 由于活动队列上时间片未到期的进程会越来越少而过期队列上的进程数量会越来越多新创建的进程都会被放到过期队列上那么总会出现活动队列上的全部进程的时间片都到期的情况这时将active指针和expired指针的内容交换就相当于让过期队列变成活动队列活动队列变成过期队列就相当于又具有了一批新的活动进程如此循环进行即可。 总结
本篇博客到这里就结束了可能我在进程概念说明中有未涉及到的点希望大家自行学习关于进程该如何控制下一篇博客再进行介绍。 .................................................................................................................苏三说我的苏三说 ————《Susan说》