【Linux】进程概念
1. 冯诺依曼体系结构
外设:
- 输入设备:鼠标,键盘,摄像头,话筒,磁盘,网卡
- 输出设备:显示器,播放器硬件,磁盘,网卡
有的设备是纯的输入,输出,也有即使输入,又是输出设备
cpu/中央处理器:
- 运算器:对我们的数据进行计算任务(算数运算,逻辑运算)
- 控制器:对我们的计算硬件流程进行一定的控制
输入设备、输出设备、运算器、控制器、内存都是独立的个体。各个硬件单元必须用“线”链接起来(“线”即总线,包括系统总线和I0总线)
计算机存储金字塔:
2. 操作系统
2.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序等等)
2.2 设计OS的目的
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
2.3 定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件
2.4 如何进行 “管理”
2.5 操作系统管理硬件
- 描述起来(指描述底层硬件),用struct结构体
- 组织起来(将描述出来的底层硬件组织起来),用链表或其他高效的数据结构
2.6 系统调用和库函数概念
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发
3. 进程
3.1 基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
- 进程:内核PCB数据结构对象+你自己的代码和数据
3.2 PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是:** task_struct**
- PCB是一个struct结构体
3.3 task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
3.4 操作系统对进程的管理
- 一个操作系统,不仅仅只能运行一个进程,可以同运行多个进程
- 操作系统必须的将进程管理起来! 如何管理进程呢?-------->先描述,在组织
- 先描述:任何一个进程,在加载到内存的时候,形成真正的进程时,操作系统都要先创建描述进程属性的结构体对象— PCB、process ctrlblock、进程控制块
- 后组织:在操作系统中,对进程进行管理,变成了对单链表进行增删改查!
3.5 组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里
3.6 查看进程
- 进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹 - 大多数进程信息同样可以使用top和ps这些用户级工具来获取
结束进程:
# kill -9 XXX // XXX:进程对应的PID
3.7 通过系统调用获取进程标示符
- 进程id(PID)
- 父进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
3.8通过系统调用创建进程-fork初识
fork:通过系统调用函数,让父进程创建子进程
fork函数返回值:
如果成功,给父进程返回子进程的PID,0返回给子进程,此时返回值有两个
如果失败,给父进程返回-1,不返回给子进程
fork 之后通常要用 if 进行分流:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("begin: 我是一个进程,pid: %d!, ppid: %d\n", getpid(), getppid());
int ret = fork();
if(ret < 0)
{
perror("fork");
return 1;
}
else if(ret == 0) //child
{
printf("我是子进程,pid: %d!, ppid: %d\n", getpid(), getppid());
}
else //father
{
printf("我是父进程 : %d!, ret: %d\n", getpid(), getppid());
}
sleep(1);
return 0;
}
代码执行结果:
- 为什么fork要给子进程返回0,给父进程返回子进程pid?
返回不同的返回值,是为了区分让不同的执行流,执行不同的代码块! 一般而言fork代码及fork之后的代码
父子共享。给父进程返回子进程pid是为了让父进程区别子进程。
- fork函数,究竟在干什么?干了什么?
- 我们为什么要创建子进程呢?
为了让父和子执行不同的事情!------需要想办法让父和子执行不同的代码块
让fork具有了不同的返回值!-------区别父子进程
- 如果父子进程被创建好,fork0,往后,谁先运行呢??
谁先运行,由调度器决定,不确定的
- 一个函数是如何做到返回两次的? 如何理解?
因为fork代码及fork之后的代码
父子共享,所以 fork 函数被父子进程执行了两次,所以有两个返回值。而且ret是父进程的数据,子进程无法修改,当子进程调用函数后,再赋值给 ret 时,发生了深拷贝,新空间也有一个变量,叫做 ret,所以是同一变量名,不同空间。
4. 进程状态
/*
* 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 */
};
-
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
-
S睡眠状态(阻塞状态): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。 -
挂起状态:或简称“挂起态”,是一个表示进程被“冻结”或“停滞”的特殊状态。在此状态下,进程不会在主存中活跃,而是被转移到辅助存储器(如硬盘)中。这意味着进程在此状态下不会获得CPU的执行时间,并从活跃队列中移除
-
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
休眠状态就是阻塞状态,我们将这种休眠状态称之为阻塞状态,于此对应有深度休眠状态,浅度睡眠的进程的会对外部的信号做出响应(比如可以直接ctrl c被终止掉)。
而深度睡眠(disk sleep)状态也是阻塞状态,这种状态的程序是针对磁盘设计的,这种状态的 进程,操作系统是不能杀掉的。原因如下:
操作系统在没有空间的时候,会通过杀掉进程节省资源,但当一个进程正在向磁盘中写入关键数据 时,如果杀掉该进程,那么写入磁盘就会失败导致数据丢失,为了应对这种情况设置了深度睡眠的状态,操作系统遇到这种状态的进程,就不会去杀掉他。直到进程向磁盘中写完关键数据后,深度睡眠才结束
-
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
-
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
4.1 Z(zombie)-僵尸进程
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态,进程的相关资源尤其是task struct 结构体不能被释放!
4.2 僵尸进程的危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,* * 换句话说,Z状态一直不退出,PCB一直都要维护。
那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。是的!因为数据结构对象本身就要占用内存,C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间! - 内存泄漏。
4.3 孤儿进程
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程(系统)领养,当然要有init进程回收喽
- 为什么孤儿进程要被领养? 因为孤儿进程未来也会退出,也要被释放
5. 进程优先级
基础概念:
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
- 如果进程长时间得不到CPU资源,该进程的代码长时间无法得到推进 — 该进程的饥饿问题
5.1 查看系统进程
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
我们很容易注意到其中的几个重要信息,有下:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值,表示进程可被执行的优先级的修正数值
5.2 PRI and NI
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别
5.3 其他概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在
多个CPU
下分别,同时进行运行,这称之为并行 - 并发: 多个进程在
一个CPU
下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
5.3.1 进程切换------>并发
进程切换就是从正在运行的进程中,收回CPU的使用权利,交给下一个要运行的进程。
通过进程切换,从而实现并发。
我们先了解一下寄存器:
- 为什么函数返回值,会被外部拿到呢?-------->通过CPU寄存器
return a -> mov eax 10 - 系统如何得知我们的进程当前执行到哪行代码了?
程序计数器pc, eip(寄存器):记录当前进程正在执行指令的下一行指令的地址! - 寄存器作用:提高进程效率,高频数据放入寄存器中
CPU寄存器
里面保存的是进程的临时数据-----进程的上下文
!
进程切换:
- 保存上下文----->下一个进程------>又到了这一个进程------>恢复上下文
- 进程在从CPU上离开的时候,要将自己的上下文数据保存好,甚至带走
- 保存的目的,是为了未来的恢复
6. 环境变量
6.1 基本概念
- 环境变量一般是指在操作系统中用来指定操作系统运行环境的一些
参数
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有
全局特性
- 全局特性的原因:我们所运行的进程,都是子进程,bash本身在启动的时候,会从操作系统的配置文件中读取环境变量信息,子进程会继承父进程交给它的环境变量!
6.2 常见环境变量
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash。
6.3 和环境变量相关的命令
- echo: 显示某个环境变量值
echo $NAME //NAME:你的环境变量名称 不加$的话,echo把NAME当作字符串,直接打印NAME
- export: 设置一个新的环境变量
//直接设置环境变量
export XL=123456 //设置的环境变量名称为XL,环境变量值为:123456
//将本地变量转化为环境变量
a=3 //设置本地变量
export a
- unset: 清除环境变量
unset NAME //NAME:你的环境变量名称
- env: 显示所有环境变量
- set: 显示本地定义的shell变量和环境变量
6.4 环境表
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
6.5 通过代码如何获取环境变量
- 命令行第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
- 通过第三方变量environ获取
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
6.6 通过系统调用获取或设置环境变量
- getenv
getenv("NAME"); //NAME:环境变量名称 返回值为环境变量的值
- putenv,下次讲解
6.7 本地变量 && 内建命令
- 本地变量,只会在本BASH内部有效,不会被继承
- 创建本地进程
- 两批命令
- 常规命令 – 通过创建子进程完成的
- 内建命令 – bash不创建子进程,而是由自己亲自执行,类似于bash调用了自己写的,或者系统提供的函数
7. 程序地址空间
7.1 内存管理和进程管理
我们可以看到的所有地址都是虚拟地址,平时写的C/C++,用的指针,指针里面的地址,全部都不是物理地址。
7.2 子进程的进程管理与内存管理
- 写时拷贝是由操作系统自动完成的,操作系统重新开辟空间,但是在这个过程中,页表左侧的虚拟地址是0感知的,一直都是不变的。
- 页表的
rw
表示该数据的读写属性,即确定了数据是否能修改。 - 子进程的页表是由父进程拷贝而来,但不同的是,不管父进程
rw
那一栏是什么,子进程页表的rw
那一栏都是r
。当子进程要修改数据时,操作系统会辨别数据是否能修改,若能修改,那操作系统会重新开辟空间,进行写时拷贝,并修改rw
属性
7.3 页表
什么是页表?
页表是一种数据结构,用于管理虚拟内存和物理内存之间的映射关系。在操作系统中,当一个程序需要访问内存时,它会先访问虚拟内存,然后再通过页表将虚拟地址映射到物理内存中的实际地址。
页表的作用是什么?
- 让进程以统一的视角看待内存。
- 增加进程虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,所以一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存
- 因为有地址空间和页表的存在,将进程管理模块,和内存管理模块进行解耦合!
页表地址:
页表地址也属于进程上下文,进程运行时,被放在了寄存器了。
7.4 大文件的惰性加载
- 概况程序在计算机如何运行:本身程序是在硬盘上,需要把程序加载进内存,然后由CPU去执行。
- 操作系统对大文件可以实现分批加载,意思就是说,先将大文件放在硬盘里,cpu运行时,需要哪部分数据或代码再把哪部分数据或代码加载到内存里,cpu用完后,再把在内存中的这部分数据或代码释放在内存中释放掉。
缺页中断
:当cpu需要某数据或代码时,会去进程地址空间查找虚拟地址,再通过页表查看对应代码或数据有没有加载到内存,如果有,就通过物理地址在内存中查找,如果没有,就在将数据或代码从硬盘加载到内存。当没有被加载的情况,就叫做缺页中断- 那么进程在被创建的时候,是先创建内核数据结构呢? 先加载对应的可执行程序呢?---------先创建内核数据结构。
原文地址:https://blog.csdn.net/2301_81073406/article/details/141980930
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!