轻松Linux-4.进程概念
屋漏偏逢连夜雨,今天就学Linux
话不多说,展示军火
1.认识冯诺依曼体系
冯诺依曼体系其实并不是什么稀罕的东西,我们生活中的笔记本、服务器、计算机等等大多都遵守冯诺依曼体系
非常经典的一张图
我们所认识的计算机,是由一个个硬件组成的
中央处理器(CPU):包含运算器、控制器等部件
输入设备:鼠标、键盘、画板等等
输出设备:显示器、打印机等等
存储器:硬盘(外存)、内存、高速缓存(CPU内的缓存,比如常听到的三缓)
2.操作系统(Operator System)
2.1 操作系统是什么
我们可以这么理解:任何计算机都包含操作系统,操作系统可以粗略分为内核和其他程序
内核:文件管理、进程管理、驱动管理、内存管理
其他程序:shell、bash、库函数等等
它的核心功能:管理系统内的各项任务、文件,是纯正的 “ 领导 ” ,就像你的校长
2.2 为什么设计OS
对底层:它与硬件交互,管理一切软硬件资源
对上层:它与用户程序(应用程序)交互,提供一个良好的软件运行环境
怎么“管理”:先描述,再组织 --先把每种数据定义成各种变量、结构体,然后通过数据结构来管理它们,例如:我是一个大学生,然后我的年龄、身高等等被写入一个个表格,然后我导就通过管理一份份这样的表格,来实现对大学生的管理
2.3 系统调用和库函数概念
再开发角度上看,操作系统对外表现为一个整体,操作体统会露出部分接口,给上层开发使用,这部分操作系统提供的接口,就是系统调用。
系统调用功能上相对比较基础,对用户下限要求较高,于是一些开发者大佬就对部分系统调用接口进行适度封装,然后就有了库,这样就更有利于上层用户使用和开发者做二次开发。
就相当于银行,它不像你直接开发金库,但是会给出服务窗口和ATM机
3.进程
3.1 基本概念与基本操作
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
3.1.2 描述进程-PCB
基本概念:
进程信息被放在一个叫进程控制块的数据结构里,可以理解成--进程属性集合
书上叫它PCB(process control block),Linux操作系统下就是task_struct
task_struct是PCB的一种
Linux系统中描述进程信息的结构体就是task_struct
task_struct是Linux内核中的一种数据结构,它包含着进程的信息,会被装载到RAM(内存)中
3.1.3 task_struct
内容分类
标识符 | 用来标识本进程的唯一标志,用于区别其他进程 |
状态 | 任务状态、退出代码、退出信息等等 |
优先级 | 相对于其它进程的优先级 |
程序计数器 | 程序中即将被执行的下一条指令的地址 |
内存指针 | 包含程序代码和进程相关信息的指针,和与其它进程共享内存的指针等等 |
上下文数据 | 进程执行时,处理器的寄存器中的数据 |
I/O状态信息 | 包括显示的 I/O请求,分配给进程的 I/O设备和被进程使用的文件列表 |
记账信息 | 可能包括处理器时间总和,使用的时钟总和,时间限制,记账号码等等 |
↓部分进程的源代码↓
还有其他很多信息,后面细说
组织进程
那么操作系统是怎么组织管理进程的呢?---------链表
在task_struct中存有链表的节点,OS通过该链表来管理进程
3.1.4 查看进程
例如,如果要查看PID为1的进程的信息,可以ls /proc/1来查看
我们可以通过系统调用获取进程标示符
进程PID,父进程PID
#include
#include
#include
int main()
{
printf("PID: %d",getpid());
printf("PPID: %d",getppid());
return 0;
}
3.1.5 创建子进程
可以用 man 2 fork来查看fork的信息
fork有两个返回值,并且父子进程共享代码数据,但是子进程是只读状态来访问父进程的代码
但子进程一旦修改写入某些变量,就会发生缺页异常(缺页中断),然后发生写时拷贝,这时
这个变量就在子进程创建了一份拷贝
这里可以看到父进程的pid:15235,子进程的pid:15236,且可以看到子进程的父进程pid,就是父进程的pid,并且父进程可以通过fork的返回值拿到子进程的pid
#include
#include
#include
int main()
{
int ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)
{
// child
printf("I am child : %d!, ret: %d
", getpid(), ret);
}
else
{
// father
printf("I am father : %d!, ret: %d
", getpid(), ret);
}
sleep(1);
return 0;
}
根据fork的返回值,我们可以用 if 来对进程进行分流 ,父进程的ret为子进程的PID,子进程的ret则为0 ( 即fork的两个返回值 )
3.2 进程状态
我们为了知道进程在干什么,我们可以查看进程的状态,进程有多种状态(在Linux内核中,进程有时也叫任务)
我们来看一下Linux内核中代码的介绍:
R运行状态:这并不意味着进程就在运行,它可能在运行,也可能在运行队列中"排队",等待被调度。
S睡眠状态:意味着进程正在等待某件事情完成,有时也叫做中断睡眠(interruptible sleep)。
D磁盘睡眠状态:有时也叫不可中断睡眠(uninterruptible sleep),这个状态的进程通常会等待IO结束。
T停止状态:可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态:这个状态只是一个 死亡状态 返回状态,你不会在任务列表里看到这个状态。
Z僵尸状态:这个状态的进程通常是在等待其父进程回收它信息,没被回收时会一直在任务列表中
3.2.1 进程状态查看
ps aux | ps axj 命令
a:显示一个终端所有的进程,包括其他用户的进程。
x:显示没有控制终端的进程,例如后台运行的守护进程。
j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等
这些指令可以组合使用如:ps aux | ps aj 、ps aux | ps uj等等
↓源于网图↓
3.2.3 僵尸进程
僵尸状态:是一个比较特殊的进程状态,上面我们提到僵尸状态下的进程通常是在等待父进程回收它的信息。
当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
即子进程成为僵尸进程,父进程要使用系统调用wait()来回收子进程的退出信息。
这里可以看到子进程29772变为了Z状态即僵尸状态
这里是源代码
#include
#include
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id > 0){ //parent
printf("parent[%d] is sleeping...
", getpid());
sleep(30);
}
else{
printf("child[%d] is begin Z...
", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
僵尸进程的危害
子进程退出时的退出状态在被回收前一定要维持下去,因为它需要将退出信息交给父进程,它需要父进程了解它任务完成的情况。也就是说,只要父进程一直不回收,子进程就会一直处于Z僵尸状态。
维护进程的退出状态本身就是要用数据维护的,也属于进程的基本信息,会被保存在task_struct(PCB)中,只要子进程一直处于Z僵尸状态,OS就要一直维护它。这就会造成内存泄漏。
因为维护子进程的退出状态要在内存中开辟空间,来暂存它的状态信息,僵尸进程一多,就会造成大量的内存资源浪费。
3.2.2 孤儿进程
如果子进程还没结束,它的父进程就退出,那子进程进入Z状态之后,该怎么办?
父进程先于子进程退出,这种情况,子进程称为“孤儿进程”
不过,这个孤儿进程会被PID为1的进程领养,即被1号init进程领养,并由1号进程回收
看段代码
#include
#include
#include
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){//child
printf("I am child, pid : %d
", getpid());
sleep(10);
}
else{//parent
printf("I am parent, pid: %d
", getpid());
sleep(3);
exit(0);
}
return 0;
}
我们可以看到父进程退出后,子进程的PPID(父进程PID)变为了 1,也就是被init进程领养了
3.3 进程优先级
基本概念:
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性
能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大 改善
系统整体性能。
查看系统进程:
在Linux系统中,用ps -l 命令会输出以下几个类似的内容:
我们可以看到,像UID、PID、PPID、PRI、NI等等
UID:代表执行者身份
PID:代表进程的代号
PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI:代表这个进程的nice值
3.3.1 优先级相关 PRI 、NI
PRI可以理解为进程的优先级,它决定了进程被CPU执行的先后顺序,PRI值越小,优先级越高
NI就是nice值,它是进程PRI的修正值,PRI与NI的关系为 PRI(new) = PRI(old) + NI
NI的取值范围为 -20~19(40个级别)
当NI为负数时,PRI就减小,优先级变高,反之更低
在Linux中调整优先级就是调整NI(nice)值,所以NI值可以影响进程PRI从而影响进程的优先级
可以用top指令来修改进程的nice值
语法 : 输入top 回车,然后按r,输入要修改进程的PID回车,而后输入nice值
还有一些其它修改nice值的指令:nice 、renice
默认普通进程静态优先级为 120
普通优先级:100〜139(我们都是普通的优先级,看nice值的取值范围,可与之对应!)
实时优先级:0〜99(不关⼼)
3.3pro 补充进程概念
竞争性:系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为 了⾼效完成任务,更合理竞争相关资源,便具有了优先级
独⽴性:多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰
并⾏:多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
并发:多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称 之为并发
3.4 进程切换
CPU上下文切换:它实际就是任务切换,或叫CPU寄存器切换,当内核决定执行其它任务的时候,它要保存当前运行进程的状态和信息,也就是在CPU寄存器中的信息。这些内容会被保存在任务(进程)自己的堆栈上,⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状况从该任务的栈中重新装⼊CPU寄存器, 并开始下⼀个任务的运⾏,这⼀过程就是context switch
如下面两张图(源于互联网)
这是Linux 0.11版本的内核代码
3.4.1 进程O(1)调度队列
在内核中存着进程的调度队列,一个CPU有一个调度队列,↓看代码↓--内核版本Linux2.6.13
都是个人理解,若有不对,请指出
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))
#define MAX_PRIO (MAX_RT_PRIO + 40)
//在Linux2.6.13内核中截取的代码
struct prio_array {
unsigned int nr_active;
unsigned long bitmap[BITMAP_SIZE];
struct list_head queue[MAX_PRIO];
};
typedef struct prio_array prio_array_t;
struct runqueue {
spinlock_t lock;
unsigned long nr_running;
#ifdef CONFIG_SMP
// 基于进程队列平均数量的cpu负载因子
unsigned long cpu_load[3];
#endif
unsigned long long nr_switches;
unsigned long nr_uninterruptible;
unsigned long expired_timestamp;
unsigned long long timestamp_last_tick;
task_t *curr, *idle;
struct mm_struct *prev_mm;
/*
struct prio_array {
unsigned int nr_active;
unsigned long bitmap[BITMAP_SIZE];
struct list_head queue[MAX_PRIO];
};
*/
// active指针指向arrays中的活跃队列
// 当前执行队列
prio_array_t *active;
// expired指针指向arrays中的过期队列
// 也就是下一轮才执行的队列
prio_array_t *expired;
// 两组队列
// 调度队列使用轮换策略
// 即执行完活跃队列,就执行过期队列
// 这时 active指针指向原过期队列
// expired指针指向原活跃队列
prio_array_t arrays[2];
int best_expired_prio;
atomic_t nr_iowait;
#ifdef CONFIG_SMP
struct sched_domain *sd;
/* For active balancing */
int active_balance;
int push_cpu;
task_t *migration_thread;
struct list_head migration_queue;
#endif
#ifdef CONFIG_SCHEDSTATS
/* latency stats */
struct sched_info rq_sched_info;
/* sys_sched_yield() stats */
unsigned long yld_exp_empty;
unsigned long yld_act_empty;
unsigned long yld_both_empty;
unsigned long yld_cnt;
/* schedule() stats */
unsigned long sched_switch;
unsigned long sched_cnt;
unsigned long sched_goidle;
/* try_to_wake_up() stats */
unsigned long ttwu_cnt;
unsigned long ttwu_local;
#endif
};
如果有多个CPU就要考虑进程个数的负载均衡问题
3.4.2 活跃队列与过期队列
每个进程都有相应的时间片,时间片还没结束的进程都会按照优先级放在活跃队列
nr_active:运行状态进程的数量
queue[140]:每一个下标位置都是一个队列,相同优先级的进程按照FIFO规则进⾏排队调度,所以, 数组下标就是优先级!(相同优先级的进程在queue的同一个下标位置,每个位置都是一条队列,有点像链式哈希表)
调度过程:
1. 从0下标开始遍历queue[140]
2. 找到第一个非空队列,该队列一定为目前优先级最高的队列(因为从0优先级开始遍历)
3. 拿到非空队列的第一个进程,开始执行,调度完成
但是O(n)的时间复杂度,还是太低效了,因此Linux设计了 bitmap[5](位图)
bitmap[5]:⼀共140个优先级、140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤ 5*32个⽐特位表⽰队列是否为空,这样,便可以⼤⼤提⾼查找效率,这样就将时间复杂度优化为O(1)
↓当然少不了好图↓
过期队列与活跃队列一样,过期队列上的进程都是时间片耗尽的进程,在活跃队列处理完后,会对过期队列上进程的时间片重新计算
runqueue查找⼀个最合适调度的进程的时间复杂度为O(1),不随着进程增多⽽导致时间成本增加,这就是Linux的大O(1)进程调度算法