linux进程概念【万字解析,深度刨析linux底层系统】

linux进程概念【万字解析,深度刨析linux底层系统】

目录

  • 一、冯诺依曼体系结构
  • 二、操作系统(Operator System)
    • 2.1 概念
    • 2.2 如何理解“管理”
    • 2.3 总结
    • 2.4 系统调用和库函数概念
  • 三、进程
    • 3.1 基本概念
    • 3.2 描述进程-PCB
    • 3.3 组织进程
    • 3.4 查看进程
    • 3.5 通过系统调用获取进程标识符
    • 3.6 通过系统调用创建进程-fork初识
  • 四、进程状态
    • 4.1 进程状态查看
    • 4.2 僵尸进程
    • 4.3 僵尸进程的危害
    • 4.4 孤儿进程
  • 五、进程优先级
    • 5.1 基本概念
    • 5.2 查看系统进程
    • 5.3 PRI and NI
    • 5.4 PRI vs NI
    • 5.5 用top命令更改已存在进程的nice:
    • 5.6 其他概念
      • 5.6.1 进程切换
  • 六、环境变量
    • 6.1 基本概念
    • 6.2 常见环境变量
    • 6.3 查看环境变量方法
    • 6.4 测试PATH
    • 6.5 和环境变量相关的命令
    • 6.6 环境变量的组织方式
    • 6.7 通过代码如何获取环境变量
      • 6.7.1 命令行第三个参数
      • 6.7.2 通过第三方变量environ获取
    • 6.8 通过系统调用获取或设置环境变量
    • 6.9 环境变量是具有全局性的
  • 七、程序地址空间
    • 7.1 程序地址空间回顾
    • 7.2 进程地址空间
      • 7.2.1 理解虚拟地址空间
      • 7.2.2 进程地址空间
      • 7.2.3 为什么存在地址空间

一、冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

截至目前,我们所认识的计算机,都是有一个个的硬件组件组成

  • 输入单元:包括键盘, 鼠标,扫描仪, 写板等
  • 中央处理器(CPU):含有运算器和控制器等
  • 输出单元:显示器,打印机等

关于冯诺依曼,必须强调几点:
这里的存储器指的是内存
不考虑缓存情况,这里的CPU(运算器 + 控制器 + 其他 = cpu)能且只能对内存进行读写,不能访问外设(输入或输出设备)
外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
一句话,所有设备都只能直接和内存打交道。


对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?

二、操作系统(Operator System)

2.1 概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

  • 内核(进程管理,内存管理,文件管理,驱动管理)
  • 其他程序(例如函数库, shell程序等等)

2.2 如何理解“管理”

  • 管理的例子
  • 描述被管理对象
  • 组织被管理对象

我们可以通过学校来类比操作系统
这样校长就是管理者,学生就是被管理者,但是校长一般都是没见过或者不会刻意见学生,但是校长是如何做管理的呢?
对管理者的理解:管理者不需要与被管理者直接交互,依旧能够把管理对象管理起来,那是因为管理的本质是对数据的管理,虽然学生不直接和校长打交道,但是学生所有的数据,早已经被校方拿走了,而且一直在更新。
但是学校学生这么多,校长是怎么管理的呢?
先描述,再组织!!!—— 数据结构

根据图上所描述的东西,其实简单来说,先描述指的就是,先面向对象的把每位学生的信息通过结构体一 一列出来,形成一个结构,然后再组织的意思就是,把每位学生的信息通过一个数据结构串起来,保证以后能够方便的维护。

2.3 总结

计算机管理硬件

  1. 描述起来,用struct结构体
  2. 组织起来,用链表或其他高效的数据结构

2.4 系统调用和库函数概念

  • 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
  • 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

    根据上图可知:
    一般我们使用操作系统的时候,我们都是调用函数,或者用指令操作,我们并不知道里面的原理或者里面干了什么,只能通过接口来和人交互。那是为什么呢?
    其实和银行类似,银行不相信任何人,操作系统也不相信任何人!因为操作系统和银行一样要保护自己,不能让人随意访问,但是也必须给上层用户提供各种服务。所以就有了类似银行的窗口一样,操作系统有自己的接口函数或者指令操作。

三、进程

3.1 基本概念

  • 课本概念:程序的一个执行实例,正在执行的程序等
  • 内核观点:担当分配系统资源(CPU时间,内存)的实体

3.2 描述进程-PCB

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
  • 课本上称之为PCB(process control block), Linux操作系统下的PCB是: task_struct

    实际上PCB就是对进程的描述,里面装有该进程的所有属性。
    task_struct-PCB的一种
  • 在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
    task_ struct内容分类
  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  • I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

3.3 组织进程


根据上图可以得知,程序在磁盘加载到内存之后,操作系统会描述对应的程序创建对应的结构体(PCB),多个程序的话,就会在组织起来,形成内核数据结构。

那为什么会有PCB(struct task_struct )结构体呢?
那是因为操作系统一贯认为管理进程,要先描述,再组织,先描述的话,就要读取对应进程的属性,那么就需要对应的PCB封装起来。

3.4 查看进程


大多数进程信息同样可以使用top和ps这些用户级工具来获取。
示例代码:

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){while(1){sleep(1);}return0;}

3.5 通过系统调用获取进程标识符

  • 进程id(PID)
  • 父进程id(PPID)

示例代码:

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){printf("pid: %d\n",getpid());printf("ppid: %d\n",getppid());return0;}

编译可以得出:

3.6 通过系统调用创建进程-fork初识

  • fork有两个返回值
  • 父子进程代码共享, 数据各自开辟空间,私有一份(采用写时拷贝)

示例代码:

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){intret=fork();printf("hello proc : %d!, ret: %d\n",getpid(),ret);sleep(1);return0;}

运行结果:

  • fork 之后通常要用 if 进行分流

示例代码:

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){intret=fork();if(ret<0){perror("fork");return1;}elseif(ret==0){//childprintf("I am child : %d!, ret: %d\n",getpid(),ret);}else{//fatherprintf("I am father : %d!, ret: %d\n",getpid(),ret);}sleep(1);return0;}

运行结果:

四、进程状态

进程状态一般有以下几种:

运行新建 就绪挂起阻塞等待 停止 挂机 死亡

  1. 普遍的操作系统层面如何理解上面的概念
  2. 什么叫运行状态?
    运行状态不只是指正在运行的状态,而是进程的PCB加载到运行队列时,不管你是不是正在运行都叫运行状态。
  3. 什么叫阻塞状态?
    当一个进程需要访问外设时,需要把这个进程的PCB从内存中的运行队列放到外设的等待队列中,等访问过后该程序的PCB会回到运行队列里,那么这个进程的PCB在等待队列时的状态叫做阻塞状态。
  4. 什么叫挂起状态?

    由上图的调度图和挂起流程可知,当内存有多个进程是内存不够,而有一个程序处于阻塞状态,不会立即调度,要等很长时间,那么操作系统会把该进程的数据和代码暂时保存到磁盘上,清理一些空间,但是该进程的PCB还在内存里,保证该进程存在,那么该进程的PCB就处于挂起状态。如果该进程准备完毕,操作系统会重新把该进程的数据和代码加载到内存。
  5. 具体的linux操作系统的状态
    为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在
    Linux内核里,进程有时候也叫做任务)。
    下面的状态在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))。
  • D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态

4.1 进程状态查看

ps aux/ps axj 命令

举例:

#include<stdio.h>#include<sys/types.h>#include<unistd.h>intmain(){while(1){sleep(1);}return0;}


通过查看,该进程处于睡眠状态。

4.2 僵尸进程

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

示例代码:

1#include<stdio.h>2#include<stdlib.h>3#include<unistd.h>4#include<sys/types.h>5intmain()6{7pid_tid=fork();8if(id==0)9{10while(1)11{12printf("子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);13sleep(1);14exit(1);15}16}17if(id>0)18{19while(1)20{21printf("父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);22sleep(2);23}24}25else26{}27return0;28}

运行结果为:

4.3 僵尸进程的危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  • 内存泄漏?是的。

4.4 孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
  • 父进程先退出,子进程就称之为“孤儿进程”
  • 孤儿进程被1号init进程领养,当然要有init进程回收喽。

示例代码:

1#include<stdio.h>2#include<stdlib.h>3#include<unistd.h>4#include<sys/types.h>5intmain()6{7pid_tid=fork();8if(id==0)9{10while(1)11{12printf("子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);13sleep(1);14}15}16if(id>0)17{18while(1)19{20printf("父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);21sleep(2);22exit(1);23}24}25else26{}27return0;28}

运行结果:

五、进程优先级

5.1 基本概念

  • cpu资源分配的先后顺序,就是指进程的优先权(priority)。
  • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
  • 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

5.2 查看系统进程

我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份。
PID : 代表这个进程的代号。
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行。
NI :代表这个进程的nice值。

5.3 PRI and NI

  • PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
  • 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
  • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  • 所以,调整进程优先级,在Linux下,就是调整进程nice值
  • nice其取值范围是-20至19,一共40个级别。

5.4 PRI vs NI

  • 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
  • 可以理解nice值是进程优先级的修正修正数据

5.5 用top命令更改已存在进程的nice:

  • top
  • 进入top后按“r”–>输入进程PID–>输入nice值

操作演示:

5.6 其他概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

5.6.1 进程切换


由上图可知,进程切换的意思就是,操作系统运行程序的时候,通过时间片,在一段时间内运行一个程序,下一段时间就运行另一个程序,每个程序执行一个时间片的时间,保证每个程序都有运行,防止卡死在一个程序内。但是进程切换的时候,进程执行到一部分的时候操作系统是怎么做的呢?这和我们离开学校去当兵类似,我们离开学校的时候学校会保留学籍,回到学校的时候恢复学籍类似。进程也是进程切换后系统会进行上下文保护,进程恢复的时候,系统会进行上下文回复。
ps:系统进行上下文保护的时候,是保护的寄存器内的数据!寄存器内的数据,只属于当前运行的进程!

六、环境变量

6.1 基本概念

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。

6.2 常见环境变量

  • PATH : 指定命令的搜索路径
  • HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • SHELL : 当前Shell,它的值通常是/bin/bash。

6.3 查看环境变量方法

echo $NAME//NAME:你的环境变量名称

示例:

6.4 测试PATH

  1. 创建hello.c文件
#include<stdio.h>intmain(){printf("hello world!\n");return0;}

将我们的程序所在路径加入环境变量PATH当中, export PATH=$PATH:hello程序所在路径。
输出结果:

6.5 和环境变量相关的命令

  1. echo: 显示某个环境变量值
  2. export: 设置一个新的环境变量
  3. env: 显示所有环境变量
  4. unset: 清除环境变量
  5. set: 显示本地定义的shell变量和环境变量

6.6 环境变量的组织方式


每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。

6.7 通过代码如何获取环境变量

6.7.1 命令行第三个参数

示例代码:

1#include<stdio.h>2#include<string.h>3#include<stdlib.h>45#define USER"USER"6#define MY_ENV"myval"7#define MYPWD"PWD"8intmain(intargv,char*argc[],char*env[])9{10//NULL->011for(inti=0;env[i];i++)12{13printf("env[%d]:%s\n",i,env[i]);14}15return0;16}

结果:

6.7.2 通过第三方变量environ获取

示例代码:

#include<stdio.h>intmain(intargc,char*argv[]){externchar**environ;inti=0;for(;environ[i];i++){printf("%s\n",environ[i]);}return0;}

结果:

libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。

6.8 通过系统调用获取或设置环境变量

getenv//获取环境变量

示例:

1#include<stdio.h>2#include<string.h>3#include<stdlib.h>45#define USER"USER"6#define MY_ENV"myval"7#define MYPWD"PWD"8intmain()9{10char*my_env=getenv(MYPWD);11printf("%s\n",my_env);12return0;13}

结果为:

putenv//设置环境变量

6.9 环境变量是具有全局性的

  • 环境变量通常具有全局属性,可以被子进程继承下去
    示例:
1#include<stdio.h>2#include<string.h>3#include<stdlib.h>45#define USER"USER"6#define MY_ENV"myval"7#define MYPWD"PWD"8intmain()9{10char*my_env=getenv(MY_ENV);11printf("%s\n",my_env);12return0;13}

直接查看,发现没有结果,说明该环境变量根本不存在。

  • 导出环境变量
export MYENV="1234
  • 再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!想想为什么?(为了不同的应用场景 ------ 让bash帮我找指令路径,身份认证)

七、程序地址空间

7.1 程序地址空间回顾

我们在讲C语言的时候,老师给大家画过这样的空间布局图。

来段代码感受一下。

#include<stdio.h>#include<unistd.h>#include<stdlib.h>intg_val=0;intmain(){pid_tid=fork();if(id<0){perror("fork");return0;}elseif(id==0){//child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取g_val=100;printf("child[%d]: %d : %p\n",getpid(),g_val,&g_val);}else{//parentsleep(3);printf("parent[%d]: %d : %p\n",getpid(),g_val,&g_val);}sleep(1);return0;}

输出结果为:

我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
  • 但地址值是一样的,说明,该地址绝对不是物理地址!
  • 在Linux地址下,这种地址叫做虚拟地址
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。

OS必须负责将虚拟地址转化成物理地址

7.2 进程地址空间

所以之前说‘程序的地址空间’是不准确的,准确的应该说成进程地址空间,那该如何理解呢?看图:

7.2.1 理解虚拟地址空间


由上图可知,我们可以把虚拟地址空间比作大饼,每个员工都有一张饼,并指向对应的员工,每个员工的饼互不干扰。每个员工都要管理,每个饼也要管理,都是先描述在组织。

7.2.2 进程地址空间

  1. 如何理解区域划分

对于区域划分,类似于划分三八线一样,每个部分都有一个区域(和数学的区间一样)。

由上图可知,每个进程(task_struct)都有一个地址空间,每个地址空间有一个结构体(struct mm_struct),里面保存了每个区域的开始地址和结束地址。

2. 区域调整
也很简单,两个区域假如,每个都让5厘米,那中间的10厘米,就是缓冲区。如果其中一个区域需要更大的空间,另一个区域可以让给这个区域,让这个区域更大,这就叫做区域扩大。

3. 进程地址空间和内存的关联(虚拟地址对物理地址的映射,通过页表来完成)

通过下图得知,进程1,进程2分别是两个儿子,他们分别都认为有4GB的的进程地址空间,他们互不干扰,他们只看得到自己的虚拟地址空间,看不见真正的物理内存。

7.2.3 为什么存在地址空间

  1. 如果让进程之间直接访问物理内存,万一进程越界非法操作呢?非常不安全。所以页表对于非法操作会进行拦截。所有程序都要遵守。
  2. 重新回到最初的问题:为什么父子进程,输出地址是一致的,但是变量内容不一样呢?由下图可知,地址空间的存在,可以更方便的进行进程和进程数据代码的解耦,保证了进程独立性这样的特征。
  3. 让进程以统一的视角,来看待进程对应的代码和数据等各个区域,方便编译器也以统一的视角来进行编译。

由上图可知,早在编译的时候,内部的代码早就有了对应的虚拟地址,然后虚拟地址空间的mm_struct结构体就会有对应代码的开始地址和结束地址。然后当你把代码加载到内存的时候,他们也就具备了物理地址,拥有两套地址,加载完之后,cpu就会把mm_struct的地址加载到他自己里面,然后通过地址查页表,通过页表映射到物理地址,调用对应的函数。在访问的过程中cpu没有见到过物理地址。