lesson3-Linux进程概念

1. 冯诺依曼体系结构

  •  存储器:内存
  • 输入设备:键盘,摄像头,话筒,磁盘,网卡...
  • 输出设备:显示器,音响,磁盘,网卡
  • 运算器算术运算,逻辑运算
  • 控制器:CPU是可以响应外部事件,协调外部就绪事件,比如:拷贝数据到内存
  1.  CPU读取数据(数据+代码),都是要从内存中读取,站在数据的角度,我们认为CPU不和外设直接交互
  2. CPU要处理数据,需要先将外设中的数据,加载到内存,站在数据的角度,外设直接只和内存打交道

总结:程序要运行,必须要先被加载到内存中(  体系结构决定的!)

2. 操作系统

  •  操作系统的定位是:一款纯正的“搞管理”的软件,并且给用户提供一个稳定,安全,简单的执行环境
  • 这里的管理:是对被管理对象的数据的管理 ,
  • 管理是先描述,再组织 ,这样的话,OS中一定会存在大量的数据结构和代码、
    • 描述起来,用struct结构体
    • 组织起来,用链表或其他高效的数据结构

 3. 系统调用

  • 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做 系统调用 。 
    • 对于操作系统来说所有人都不值得信任,而又需要访问操作系统,所以出现了 系统调用
    • system call本质:就是 C语言提供的函数,只不过是系统调用的

4.进程 

进程 = 对应的代码和数据 + 进程对应的PCB结构体

文件 = 内容 + 数据 

  • 被加载到内存中的程序(.exe)叫做进程,在Linux中,运行一条命令,该命令运行的时候,其实就是在系统层面创建了一个进程

4.1 描述进程的PCB

  • Linux是可以同时加载多个程序的,是可能同时存在大量的进程在系统中的(OS,内存),是需要管理大量的进程,
  • 进程信息被放在一个叫做 进程控制块的数据结构 中,可以理解为进程属性的集合
    • 课本上称之为 PCB (process control block),Linux操作系统下的 PCB 是: task_struct
    •  task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息

4.2 task_ struct内容分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据[要加图CPU,寄存器]。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

 4.3 查看进程(ls /proc)

查询进程命令 ps axj | head -1 && ps axj | grep '进程名' 

  •  进程的信息可以通过 /proc 系统文件夹查看

 

  •  这里需要开两个终端,其中一个终端需要中的myproc需要死循环,方便观察

4.4 获取进程标示符(getpid() && getppid())

  • pid子进程,ppid父进程,在一个终端中父进程不变,子进程的pid会变
  • getpidgetppid是系统调用函数

 4.5 创建进程 - fork

  • fork函数的返回值有两个返回值
  • 失败:  -1
  • 成功:a. 给父进程返回子进程的pid b. 给子进程返回0

 4.5.1 使用if进行分流

 

  • id在父进程里面是子进程的pid,而在子进程里面是0

4.5.2 关于fork的返回值问题

为什么fork返回值会给父进程返回子进程的pid,给子进程返回0?

  • 对于进程来说父亲只有一个,而儿子可以有多个,父进程:子进程 = 1:n

为什么fork会有两个返回值?

  1. 因为fork内部,父子各自会执行自己的return语句
  2. return被执行两次,就会产生两个返回值,但是并不意味着会保存两次,

  • 当我们已经准备return了,我们的核心代码其实这时已经执行完了
  • 操作系统和cpu运行某一个进程,本质从task_struct 形成的队列中挑选一个task_struct,来执行它的代码,

fork中父子进程被创建出来,哪一个进程先运行?

  • 谁先运行,不一定,这个是由操作系统的调度器决定的

4.6 进程状态

  1. 运行状态:task_struct 结构体在运行队列中排队,就叫做运行态
  2. 阻塞状态等待非CPU资源就绪,比如scanf输入时,我们没有输入,就会出现这种状态
  3. 挂起状态当内存不足的时候,OS通过适当的置换进程的代码和数据到磁盘,就会出现这种状态
/*
* 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*/
};
  • R:对应上面的运行态
  • S:对应的就是上面的阻塞状态,可中断睡眠,等待非CPU资源就绪
  • D:睡眠状态,磁盘睡眠,深度睡眠,不可被中断,不可以被被动唤醒
    • 如果没有这个状态,那么当服务器压力过大的时候,OS会通过一定的手段,杀掉一些进程,来起节省空间的作用,但是可能会失去一些数据
  • T:暂停状态,通常在调试的时候出现
  • Z:僵尸状态
    • 一个进程已经退出,但是还不允许被OS释放,处于一个被检测的状态,就叫做僵尸状态
    • 存在这状态的主要的目的是:一般是为了父进程或者OS回收

4.6.1 运行状态-R

  • 一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程 

  • 所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行

4.6.2 浅度睡眠状态-S 

  • 检测脚本:while :; do ps ajx | head -1 && ps axj | grep myproc | grep -v grep; sleep 1; echo "**********************************"; done
  • 因为系统中不止一个进程,所以这里的状态是S阻塞状态
    • S+,中的+表示前台,这时输入各种linux命令是没有用的

 

  • 后台程序(&),可以输入各种linux命令 

4.6.3 深度睡眠状态-D

  • 一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。
  • 该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束 

4.5.4 暂停状态-T 

  • kill -SIGSTOP 进程id : 让进程进入T状态
  • 这个状态也会在调试,打断点的时候出现

4.6.1 模拟实现->僵尸进程 

  • 如果一个父进程创建了很多子进程,且不读取也不回收子进程,那么就会造成内存资源的浪费,就是内存泄漏的问题,毕竟数据结构 对象本身就要占用内存,

4.6.2 模拟实现->孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,子进程就变成了“孤儿进程
  • 孤儿进程被1号init进程领养,当然也会由init进程(系统本身)回收
  • kill 9 进程的pid   杀死进程
  • kill 19 进程的pid  暂停进程

4.7 进程优先级

cpu资源分配的先后顺序,就是指进程的优先权(priority)
存在优先级就可以将重要的进程先执行,不重要的进程后执行,可以极大的改善系统进程

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  • NI :代表这个进程的nice

4.7.1 PRI vs NI

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

4.7.2 更改进程PRI

方法一: 使用top 

使用top命令后按“r”键,会要求你输入待调整nice值的进程的PID

输入进程PID并回车后,会要求你输入调整后的nice值

输入nice值后按“q”即可退出,如果我们这里输入的nice值为10,那么此时我们再用ps命令查看进程的优先级信息,即可发现进程的NI变成了10,PRI变成了90(80+NI)

  • 这里的nice的取值范围在-20到19之间,每次通过nice修改PRI时,PRI都是从80开始改变

  • 一个进程的优先级 = 老的优先级 + nice 值
  • 若是想将NI值调为负值,也就是将进程的优先级调高,需要使用sudo命令提升权限

方法二: 使用renice命令

使用renice命令,后面跟上更改后的nice值和进程的PID即可。

ps命令查看进程的优先级信息,也可以发现进程的NI变成了10,PRI变成了90(80+NI)

4.8 进程的四个重要概念

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

4.9 CPU中进程的切换方式

  •  进程A正在运行,且会把自己的数据放在寄存器中,所以CPU里面一定保存的是进程A的临时数据,也叫做上下文数据
  • 进程A正在运行,如果此时需要从进程A切换成进程B,那么进程A就需要带走自己的上下文数据,主要是为了下次回来的时候,能恢复上去,并继续按照之前的逻辑继续向后运行,就如同没有中断过一样,
  • CPU内的寄存器只有一份,但是上下文可以有多份,分别对应不同的进程

5. 环境变量

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数

假设我们想用C写一个输出 ‘hello world’ 的程序

  •  './' 是什么东西?test.o从构成上,也可以认为是一个命令
  • 问题:为什么不能直接test.o?而ll就可以直接用,而不需要带./

5.1 环境变量PATH

  •  是一个路径集,命令再被执行时,系统会在环境变量PATH中进行路径查找,如果找到,就停止查找,执行命令。

这也就解释了,为何ls不需要带路径,因为ls所在的路径,本身就在PATH环境变量列表当中。

  • 在系统中,可执行程序的搜素路径是保存在一个“”全局“”变量中,比如这里的PATH,
  • 环境变量PATH:从左向右依次进行程序搜素,找不到就继续下一条路径,最后找到了就停下来,找不到就command not found!

5.2 新增命令到环境变量中 

5.2.1 方案一:不推荐

cp test.o /usr/bin

  • 直接把自己新增的命令放在PATH下的某个路径里面
  • 这样做也行,不过会污染别人写好的指令集,

5.2.2 方案二:推荐

 把当前路径添加到环境变量中

  • 这就是给PATH新增了一个路径,推荐
  • 如果重新启动终端,就会清除刚才添加到PATH中的路径,

5.3 获取环境变量 

 5.3.1 方案一: 环境变量参数

  • 通过命令行第三个参数获取环境变量

 5.3.2 方案二:借助第三方全局变量

 

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

 5.3.3 方案三: 借助系统调用

  •  常用getenvputenv函数来访问特定的环境变量

5.4 特性一: 全局属性

  • 子进程的环境变量是从父进程那里继承来的,默认所有的环境变量都会被子进程继承,这也是环境变量具有全局属性的原因

 解释main函数的前两个命令行参数

  • argc决定命令个数,argv决定命令选项
  •  存在命令行参数,就可以解决为什么ll -al可以带子选项的问题了

6. 程序地址空间

6.1 验证一: 地址空间排布

注意下面验证的程序地址空间不是内存

  • static修饰局部变量,会将该变量开辟在全局区域
  • 指针 = 堆,&指针变量 = 栈
  • 注:上述代码在linux和vs中的结果可能不一样,

 6.2 验证二: 程序地址空间是否是真实的物理空间

  •  同一个地址是不可能出现两个不同的值,所以这里一定不真实的物理地址空间
  • 几乎所有的语言,如果他有地址的概念,这个地址一定不是物理地址,而是虚拟地址

6.3 理解地址空间

内核中的地址空间,本质上就是一种数据结构

  •  以前的计算机是直接访问物理内存
    这种方式很有可能让别人猜到重要的数据存放的地址空间,
  • 我们直接使用物理内存,特别不安全,内存本身是随时可以被读写

6.3.1 现代CPU访问物理地址的方式 

 CPU永远拿的都是虚拟地址,不断跳转

 

  •  上面这个是现代计算机使用的方案,
  • CPU访问物理地址,会先访问虚拟地址,中间通过一种映射机制,再去访问物理地址,
  • 中间的映射机制起到了保障作用,如果虚拟地址是非法的,它会起到禁止访问,

6.3.2 页表的引入 

 

  •  地址空间和页表(用户级)是每一个进程都私有一份
  • 只要保证,每一个进程的页表,映射的是物理内存的不同局域,就能做到,进程之间是不会互相干扰,保证进程的独立性

6.3.3 解释父子进程同一地址的不同值 

  •  这张图可以解释验证程序地址空间是否是真实的物理空间中为什么同一个地址出现了两个不同的值
  • return会被执行两次,本质就是发生了写时拷贝,所以父子进程各自在物理内存中,都有属于自己的变量空间!只不过在用户层用同一个变量(虚拟地址!)来标识

 6.3.4 理解虚拟地址

  •  当我们的程序,在编译的时候,形成可执行程序的时候,加载到内存中的时候,已经形成了地址
  • 地址空间在OS内部需要遵守,编译器在OS内部也需要遵守,
    • ​​​​​​即编译器编译代码的时候,就已经给我们形成了各个区域代码区,数据区.....
    • 它们采用了和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址
    • 故程序在编译的时候,每一个字段早已经具有了一个虚拟地址

6.4 存在 地址空间 && 页表 的理由

6.4.1 理由一 

凡是非法的访问或者映射,OS都会识别到,并终止这个进程,有效的保护物理内存

  •  地址空间和页表是OS创建并维护的,这就意味着凡是想使用地址空间和页表进行映射,也一定会在OS的监管之下来进行访问,
  • OS的这种监管也保护了物理内存中的所有合法数据,包括各个进程,以及内核的相关数据

6.4.2 理由二

内存管理模块和进程管理模块形成解耦合,延迟分配的策略,避免申请物理空间又不用的情况,提高的整机的效率 

  •  因为有地址空间的存在,就有页表的映射的存在,
    • 这就意味着我们可以对未来的数据进行任意位置的加载,物理内存的分配也就可以和进程的管理分开了
    • 上层申请空间,物理地址会一个字节都不给,只要当你真正进行对物理地址空间访问的时候,才会执行内存的相关管理算法,帮你申请内存,构建页表映射关系(这些都是由操作系统自动完成的)

6.4.3 理由三 

进程独立性 可以通过 地址空间 + 页表的方式实现

 

  •  因为有页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,在进程视角所有的内存分布都是有序的
  • 因为有地址空间的存在,每一个进程都认为自己拥有4GB空间(在32位下),并且各个区域是有序的,进而可以通过页表映射到不同的区域(每一个进程都不知道,也不需要知道其他进程的存在)

重新理解挂起状态

  • 加载的本质 就是创建进程,但并不是必须立马把所有的程序代码和数据加载到内存中,并创建内核数据结构建立映射关系
  • 理论上程序是可以分批加载,也可以分批换出,而进程的数据和代码被换出就叫做挂起
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值