进程
基本概念
课本概念:程序的一个执行实例,正在执行的程序等内核观点:担当分配系统资源( CPU 时间,内存)的实体
我们知道,我们在编译完程序之后会生成一个可执行文件,这个可执行文件会有两部分内容:内容+属性,当我们将这个文件运行起来,本质其实就是将其加载到了内存中,而程序文件加载到内存中,就变成了进程,而这个进程的组成也就变为了:对应的文件+进程属性
描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为 PCB ( process control block ), Linux 操作系统下的 PCB 是 : task_struct
我们知道,我们的电脑中同时会有大量进程在运行中,而对应的我们的OS就需要对其进行管理,提到管理,就又是我们那6个字,先描述,再组织!那么我们的操作系统是如何对进程进行描述的呢?其实就是利用我们的PCB,可以理解为进程属性的集合,我们的操作系统将PCB通过双向链表的方式将各个进程组织起来方便管理,例如我们若想创建一个进程,其实就是将这个进程的代码与数据加载进内存,再将这个进程的信息存入PCB,链入双向链表中,若想退出一个进程,事实就是将这个进程先从PCB中摘下,再将内存中的数据释放,而在Linux中的PCB就是我们的task_struct
我们如果想查看系统中的进程,可以使用ps aux命令
描述进程的数据结构,就是一批结构体对象
进程:可执行程序与管理进程需要的数据结构的集合
task_struct-PCB的一种
在 Linux 中描述进程的结构体叫做 task_struct 。task_struct 是 Linux 内核的一种数据结构,它会被装载到 RAM( 内存 ) 里并且包含着进程的信息
task_ struct内容分类
标示符 : 描述本进程的唯一标示符,用来区别其他进程。状态 : 任务状态,退出代码,退出信号等。优先级 : 相对于其他进程的优先级。程序计数器 : 程序中即将被执行的下一条指令的地址。内存指针 : 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针上下文数据 : 进程执行时处理器的寄存器中的数据 [ 休学例子,要加图 CPU ,寄存器 ] 。I / O 状态信息 : 包括显示的 I/O 请求 , 分配给进程的 I / O 设备和被进程使用的文件列表。记账信息 : 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。其他信息
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。
查看进程
进程的信息可以通过 /proc 系统文件夹查看如:要获取 PID 为 1 的进程信息,你需要查看 /proc/1 这个文件夹。
我们使用/proc命令将系统中的进程信息打出来会发现有很多数字,其实这些数字就是进程的PID,其对应的文件中存储着进程的其他信息,我们可以采用proc/1的形式来查看进程PID为1的进程信息
大多数进程信息同样可以使用top和ps这些用户级工具来获取
单独使用ps指令,可以显示所有进程信息
这些进程信息有一些是我们并不需要的,我们可以加上grep命令过滤掉不需要的信息
通过系统调用获取进程标示符
进程 id ( PID )父进程 id ( PPID )
我们可以使用系统调用函数getpid(),getppid()来分别获取父进程与子进程的id
运行后就会打印该进程的PID与PPID
当我们使用ps命令查看进程信息时也会发现PID为30190
通过系统调用创建进程-fork初识
fork 有两个返回值父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
我们先来在我们之前的代码中加上fork
我们发现运行的结果有一些不一样了,加上fork之后是两个两个运行,第一行是该进程的PID与PPID,第二行是fork创建的子进程,我们可以发现,fork创建的进程的PPID就是proc进程的PID,也就说明fork其实是proc的子进程,他们两个是父子关系
我们这里有一个问题,子进程的PPID是父进程,那么父进程的PPID又是什么呢?这里就引入了我们的bash,可以理解为所有进程的最终父进程,bash创建子进程交给子进程,bash又叫做命令行解释器,通过创建子进程让子进程去完成相应的任务
我们使用fork()去创建一个进程
我们发现,父进程的返回值是子进程的PID,而给子进程的返回值则为0,若申请失败则会返回-1
值得注意的是:其实我们的fork函数调用之前的代码默认父进程执行,而调用之后的代码则是父子进程共同执行,虽然代码是共享的,但是数据域却是独立的,而且当我们开始执行父子两个进程时,其先后顺序我们也是不确定的,取决于操作系统算法的处理
那么既然我们的父子进程共用一份代码,那要是让父子进程共同去做一件事,创建父子进程不就没有意义了吗?其实是可以这么理解的,所以我们通常情况下都是在使用fork后加上if else条件判断来使两个进程分别去执行不同的代码
fork问题总结:
fork为什么会有两个返回值?
事实上,原本的fork只有一个返回值return pid,但是因为在返回之前就通过fork将子进程创建好了,子进程也需要返回一个pid,所以给我们看到的就是返回了两个pid
fork父进程的执行顺序与代码数据的复制问题?
这个我们上面其实已经提到了,父进程与子进程共享代码,独立数据,所以返回值不一样,这也体现了进程的独立性,而运行顺序则是由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 * const task_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 */
};
这些状态信息都是保存在task_struct当中的
进程状态查看
调用ps aux / ps axj 命令
我们可以看到进程中有R状态的,有S状态的
R 运行状态( running ) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。S 睡眠状态( sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠( interruptible sleep ))。
R状态与S状态不难理解,我们的操作系统在运行进程时从运行队列中拿取可运行的进程进行运行,而这个R状态就是指这个进程在队列中或者正在被运行的状态,而S状态我们可以理解为浅睡眠状态(因为一会有个深睡眠状态),则是与R状态相对的,可以随时被唤醒或者被杀死的状态
我们可以先来看一段代码
我们将函数死循环,运行时打开另一个窗口,此时我们查看它进程的状态
我们可以发现proc进程变为了S状态,那么我们就有疑问了,我们不是一直在运行吗,不应该是R状态吗,为什么是S呢?事实上,因为我们的CPU处理速度很快,proc进程仅仅只是打印,CPU长时间处于等待外设打印完,所以这里的S状态就是展现的此时CPU已经工作完毕,展现出来的S状态
处于S状态的进程是可以被杀死的,可以使用kill+进程id将其杀死
D 磁盘休眠状态( Disk sleep )有时候也叫不可中断睡眠状态( uninterruptible sleep ),在这个状态的进程通常会等待IO 的结束。
D这个状态我们也可以称之为深度睡眠状态,当一个进程处于D状态时,则无法被杀死,只有该进程自动唤醒才可结束,例如:某进程要求对磁盘进行写入,那么在写入期间该进程就处于深度睡眠状态,不会被杀死,因为该进程需要等待磁盘的回复
T 停止状态( stopped ): 可以通过发送 SIGSTOP 信号给进程来停止( T )进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
我们通过kill -SIGSTOP 26705停了下来,再用kill -SIGCONT 26705将其恢复
Z(zombie)- 僵尸状态(僵尸进程)僵死状态( Zombies )是一个比较特殊的状态。当进程退出并且父进程(使用 wait() 系统调用 , 后面讲)没有读取到子进程退出的返回代码时就会产生僵死 ( 尸 ) 进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入 Z 状态
对于僵尸状态,我们可以这么理解,就是当一个进程死亡了之后,先不将其清理掉,而是先进行信息采集(类似于验尸),当采集完信息之后,再将其清理掉,而这个死亡到被清理中间信息采集的状态,就叫僵尸状态
而具体是如何进行操作的呢?其实就是再进程中,子进程先行退出,父进程还在运行,但是父进程未读取到子进程的状态,所以当子进程退出之后,父进程会到PCB中去收集子进程信息,调查子进程退出的原因,(1.是否正常运行完毕,2.是否异常?3.发生了什么异常)方便调查,当读取完毕之后子进程就变成了X状态,我们来演示一个僵尸进程
我们可以看到开始父子进程一起运行,5秒后子进程退出,只剩父进程,我们设置一段脚本来监视进程
while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
我们可以发现,当子进程退出后,就变为了僵尸进程
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z 状态维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在 task_struct(PCB) 中,换句话说,Z 状态一直不退出, PCB 一直都要维护一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,C 中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!会造成内存泄漏
孤儿进程
在我们Linux大多数进程中都有着子进程与父进程,但是其中有一类特殊的进程,父进程因为某种原因先行退出,只留下了子进程,而此时的这个子进程就被称之为孤儿进程
那么此时就有一个问题了,如果父进程提前退出了,当这个子进程退出时,没有父进程给他回收资源,造成内存泄露,该怎么办呢?
事实上,我们的操作系统对这种情况作了专门的处理,给孤儿进程找了个爹,也就是我们的1号进程,1号进程关乎操作系统的正常运行,不会挂掉,是个靠谱的爹,此时我们的孤儿进程也就变成了1号进程的子进程,不再孤儿了
我们可以看到,当我们的父进程结束了之后,子进程的PPID就变成了1,也就是我们的1号进程
进程优先级
基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的 linux 很有用,可以改善系统性能。还可以把进程运行到指定的 CPU 上,这样一来,把不重要的进程安排到某个 CPU ,可以大大改善系统整体性能
进程优先级,顾名思义,就是对于做这件事情的先后顺序,进程优先级创建就是为了平衡我们操作系统与不同进程间的顺序关系,因为CPU的资源是有限的,一个CPU一次跑一个进程,所以就给了进程优先级的属性,可以决定急得先跑,不急的后跑
查看系统进程
我们就可以看到进程的基本信息了
UID : 代表执行者的身份PID : 代表这个进程的代号PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号PRI :代表这个进程可被执行的优先级,其值越小越早被执行NI :代表这个进程的 nice 值
而跟我们进程优先级相关的两个参数则是PRI 与NI
PRI and NI
PRI 也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被 CPU 执行的先后顺序,此值越小进程的优先级别越高NI 就是我们所要说的 nice 值了,其表示进程可被执行的优先级的修正数值PRI 值越小越快被执行,那么加入 nice 值后,将会使得 PRI 变为: PRI(new)=PRI(old)+nice这样,当 nice 值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在 Linux 下,就是调整进程 nice 值nice 其取值范围是 -20 至 19 ,一共 40 个级别
值得注意的是:
1.默认PRI值为80,NI为0.
2.我们优先级的计算公式是PRI(new)=PRI(old)+NI,PRI为基础数据,NI为修正数据,注意,修正数据二次修正后仍与最开始的PRI进行计算
用top命令更改已存在进程的nice:
top进入 top 后按 “r”–> 输入进程 PID–> 输入 nice 值
输入top命令可以查看任务管理器,实时更新资源占用情况,此时我们按r,然后输入修改的PID,之后加上NI值就可以修改了
其他重要概念
竞争性 : 系统进程数目众多,而 CPU 资源只有少量,甚至 1 个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级独立性 : 多进程运行,需要独享各种资源,多进程运行期间互不干扰并行 : 多个进程在多个 CPU 下分别,同时进行运行,这称之为并行并发 : 多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
对于进程的而言,多个进程同时运行其内部实现其实是通过一个叫做时间片的东西,CPU通过时间片轮转的方式去决定先去执行哪个进程,但是因为时间片分配给CPU的时间足够短,所以我们才会感觉是在同时运行的
环境变量
基本概念
环境变量 (environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++ 代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
环境变量其实就是系统自带的具有全局性质的一些命令
常见环境变量
- PATH: 指定命令的搜索路径。
- HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)。
- SHELL: 当前Shell,它的值通常是/bin/bash。
查看环境变量的方法
echo $NAME//NAME: 你的环境变量名称
我们以PATH为例子先来查看环境变量
我们可以看到,当我们输入了echo $PATH时,显示的是指定命令的搜索路径
以及我们的HOME与SHELL命令 ,此时我们可以发现,echo+$其实与我们的取地址类似,取其中的内容
和环境变量相关的命令
1. echo: 显示某个环境变量值2. export: 设置一个新的环境变量3. env: 显示所有环境变量4. unset: 清除环境变量5. set: 显示本地定义的 shell 变量和环境变量
测试PATH
其实我们一直有一个问题,那就是为什么有些命令不需要加./而有的命令需要加./呢?
事实上,就是归功于我们的PATH,我们编写的程序一般是需要指定文件目录,先找到他在哪里的,而我们的系统命令则是通过PATH来寻找,而当我们自己去执行我们自己的可执行程序时,系统是不知道目录的,所以才需要我们加上./
我们再来看这个PATH,其显示的就是寻找路径,在这个路径下寻找目录,若找到,则执行命令,未找到,则报错
那我们系统命令可以通过PATH来省略路径,那我们可不可以让我们自己的程序不带路径也能执行呢?
答案是可以的
1.将可执行程序的绝对路径加到PATH路径下,不过这个方式是Linux特有的
此时我们的路径就被加入到了 ,我们再运行就不需要路径了
2.将可执行程序加到PATH的某个路径下(此方法会污染系统命令,不推荐)
那么为什么这些命令系统是如何到环境变量中可以自动识别呢?
是因为安装软件时会将那个软件的可执行程序拷贝到PATH的某个路径下
环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
通过代码如何获取环境变量
其实我们的main()函数并不是没有参数的,而是有三个参数,因为我们不常使用,所以没有写
其实我们的命令行参数实际是一个字符指针数组,指向一个个命令行参数的字符串
下面我们来看这样一段代码
所以我们的命令是可以传入main函数而进行判断实现不同的操作的,但事实上没有main函数的参数传入,这三个参数也是存在的,是由系统获取调用的
当我们的env[i]变为NULL时会自动停止
我们可以看到,显示的都是各个环境变量的值
我们除了使用main函数的第三个变量来获取环境变量以外,我们还可以通过第三方变量environ来获取
这段代码同样也可以获取到环境变量的值,不过因为libc中定义的全局变量environ只想环境变量表,environ没有包含在任何头文件中,使用时需要用extern进行声明
通过系统调用获取环境变量
我们除了通过函数的第三个参数以及第三方变量environ来获取环境变量外,还可以通过系统调用getenv来获取环境变量
我们使用getenv获取了PATH环境变量的值
环境变量通常具有全局属性
环境变量通常具有全局属性,可以被子进程继承下去
运行后发现无反应,说明该环境变量不存在
不过当我们导出环境变量时就存在了
原因是proc是bash的子进程,导入bash,proc也就可以使用了
程序地址空间
我们在之前的学习中一定见过这幅图,这就是我们的进程地址空间,我们先来看一段代码来回顾下我们的各区大致地址
我们可以看到,地址从上往下依次增加,符合我们原来对于进程地址空间的理解
接下来我们来看另一段代码
我们可以看到,子父进程全局变量的值与地址是相等的,我们可以想到,因为他们代码复用,子进程未改变变量
但是此时如果我们稍作改动,将g_val的值改为100,再保证子进程先执行
此时我们会发现,父子进程的值不同了,但是地址却还相同,这可太不合常理了,我们之前的认知,在进程中值是存储在内存中的,我们的进程地址空间对应了我们的内存,而我们此时父子进程地址相同,值却不同,这在我们的认识中一定是不可能的,那又是为什么呢?
事实上,对于这个问题,我们需要从进程地址空间开头说起,首先就有一个问题,我们所看到的,打印出的地址是物理地址吗?从我们之前的认知来看,是的,但是如果是物理地址的话此时的父子进程访问同一个地址却得出了不同的值,这是不可能的,所以我们认知的地址,并不是真实的物理地址,而是虚拟地址!!这很重要,物理地址我们是无法看到的,是由操作系统进行管理的
所以我们在之前打印父子进程的地址时,打印的是虚拟地址,他们是相同的,但实际上在底层,物理地址上,并不相同
进程地址空间
我们的操作系统对父子进程的虚拟地址进行查看,在分别对应的页表中去与实际地址对应,而我们的页表其实除过虚拟地址与实际物理地址,还有其他的管理权限等信息
那么此时我们再来回答3个最基本的问题
什么是进程地址空间?
进程地址空间实际上就是我们之前看到的那张表,其本质其实是内存中一种内核数据结构,在Linux中是用mm_struct实现,而这个数据结构自下而上的记录了我们程序的各个大区,堆,栈,代码,常量等等等等
进程地址空间是如何工作的?
实际上,就是操作系统通过进程地址空间中的虚拟地址,去对应操作系统中的页表,存储在实际的物理空间中
为什么会有进程地址空间
1.为了使空间分配更加合理,通过虚拟内存的方式让操作系统去间接在物理内存上开辟,如果直接在物理内存中开辟,相邻的两个进程进行扩容时就有可能使一个进程的空间不连续,数据实际上在内存中是跳跃放置的
2.保护物理空间
进程地址空间的虚拟内存通过页表会有一个一一对应的关系,我们如果都在物理内存中操作,当存在一个野指针访问时,可能会直接访问到其他数据,但当我们有了虚拟内存,对于地址陌生的指针,我们可以直接报错,简洁地保护了我们的物理内存
回到之前,我们在来回看上面的问题,OS对进程管理中,存在一个进程就有一个进程地址空间,就有一个mm_struct,负责划分各个区域
而我们C/C++申请内存的本质实际上是向实际内存申请一块空间,再对应到进程地址空间中对应区域中未被使用的虚拟地址,也页表建立权限关系后返回虚拟地址,这其实是对应的逆过程
而我们PCB中会存储进程的虚拟地址,进程想拿到代码先通过页表找到物理地址再拿到代码