课本概念:程序的⼀个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合.课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct-PCB的⼀种
• 在Linux中描述进程的结构体叫做task_struct。
• task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下⼀条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I∕O状态信息: 包括显示的I/O请求,分配给进程的I∕O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
查看进程
1. 进程的信息可以通过 /proc 系统文件夹查看
2.top或者ps
ps aux / ps axj 命令
- a:显示⼀个终端所有的进程,包括其他用户的进程。
- x:显示没有控制终端的进程,例如后台运行的守护进程。
- j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
- u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等
3.通过系统调用获取进程标示符
- 进程id(PID)
- 父进程id(PPID)
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 int main()
5 {
6 while(1){
7
8 sleep(1);
9 printf("我是一个进程!我的pid:%d,我的ppid:%d \n",getpid(),getppid());
10 }
11 return 0;
12 }
~
创建进程
通过系统调用创建进程-fork初识
- fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有⼀份(采用写时拷贝)
- 常用if父子分流
操作成功,0返回给子进程把pid返回给父进程,返回给父进程-1是创建失败
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if(ret < 0){
perror("fork");
return 1;
}
else if(ret == 0){ //child
printf("I am child : %d!, ret: %d\n", getpid(), ret);
}else{ //father
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
所以我们可以用if来分流,让父子执行不同的操作
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 int main()
5 {
6 printf("程序开始运行pid:%d,",getpid());
7
8 pid_t id = fork();
9 if(id < 0){
10 perror("fork");
11 return 1;
12 }
13 else if(id == 0)
14 {
15 //child
16 while(1)
17 {
18 sleep(1);
19 printf("我是一个子进程,我的pid:%d ,我的父进程pid:%d\n",getpid(),getppid());
20 }
21 }
22 else{
23 //father
24 while(1)
25 {
26 sleep(1);
27 printf("我是一个父进程,我的pid:%d ,我的父进程pid:%d\n",getpid(),getppid());
28
29 }
30 }
31 return 0;
32 }
fork为什么会有两个返回值?
fork也是函数,它是一个系统调用,fork创建子进程,父子各一次return
两个返回值各种给父子如何返回?
fork也是函数,它是一个系统调用,fork创建子进程后,子进程进行写实拷贝父进程,父进程里的id写给子进程
写实拷贝
创建子进程中,子进程拷贝父进程的代码和页表等的数据后各自独立一份单独执行,从这里也可以间接印证,数据并不是存在实际的地址中,一个变量怎么可能会有两个值呢?只不过是一种映射方式,涉及到虚拟地址后面再谈
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 int a=100;
5 int main()
6 {
7 printf("程序开始运行pid:%d,",getpid());
8
9 pid_t id = fork();
10 if(id < 0){
11 perror("fork");
12 return 1;
13 }
14 else if(id == 0)
15 {
16 //child
17 while(1)
18 {
19 a++;
20 sleep(1);
21 printf("我是一个子进程,我的pid:%d ,我的父进程pid:%d,a:%d\n",getpid(),getppid(),a);
22 }
23 }
24 else{
25 //father
26 while(1)
27 {
28 sleep(1);
29 printf("我是一个父进程,我的pid:%d ,我的父进程pid:%da:%d\n",getpid(),getppid(),a);
30
31 }
32 }
33 return 0;
34 }
~
进程状态
运行&&阻塞&&挂起
运行:进程在调度队列中,进程的状态都是R
阻塞:等待某种设备或资源就绪
挂起:把运行队列中的进程交换到磁盘的swap交换分区里面
创建子进程是为了使子进程执行某种命令,子进程回返回父进程一种状态(可以用ps命令来查)
/*
*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睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表里看到这个状态。
僵尸进程
创建子进程是为了使子进程执行某种命令,僵尸进程是进程状态的一种,子进程一种维持在一种状态而父进程迟迟获取不到子进程具体的退出信息,容易内存泄漏!
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 int main()
6 {
7 pid_t id = fork();
8 if(id < 0){
9 perror("fork");
10 return 1;
11 }
12 else if(id > 0)
13 { //parent
14 printf("parent[%d] is sleeping...\n", getpid());
15 sleep(30);
16 }else{
17 printf("child[%d] is begin Z...\n", getpid());
18 sleep(5);
19 exit(EXIT_SUCCESS);
20 }
21 return 0;
22 }
僵尸进程的危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果⼀直不读取,那子进程就⼀直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态⼀直不退出,PCB⼀直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义⼀个结构体变量(对象),是要在内存的某个位置进行开辟空间!
那什么样的进程具有内存泄漏,是比较麻烦的?
进程退出了,内存泄漏就不存在了。但常驻内存的进程是给PCB维护的,释放不归操作系统管,内存释放就交给用户了,容易内存泄漏。
孤儿进程
父进程创建子进程,父进程在子进程未结束之前结束,子进程此时就是孤儿进程,被1号进程领养
#include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int main()
5 {
6 pid_t id = fork();
7 if(id < 0){
8 perror("fork");
9 return 1;
10 }
11 else if(id == 0){//child
12 while(1){
13 printf("I am child, pid : %dppid:%d\n", getpid(),getppid());
14 sleep(1);
15
16 }}
17 else{//parent
18 int cnt=5;
19 while(cnt--){
20 printf("I am parent, pid: %dppid:%d\n", getpid(),getppid());
21 sleep(1);
22 exit(0);
23 }
24 }
25 return 0;
26 }
~
孤儿进程使用ctrl c杀不死,因为ctrl c杀不死他是后台进程 (./cmd &也能变成后台进程)得使用 kill -9
进程优先级
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
UID : 代表执行者的身份
• PID : 代表这个进程的代号
• PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
• PRI :代表这个进程可被执行的优先级,其值越小越早被执行默认为80
• NI :代表这个进程的nice值
PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,值越小进程的优先级别越高
那NI就是nice值,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快
被执行。所以,调整进程优先级,在Linux下,就是调整进程nice值(nice其取值范围是-20至19,⼀共40个级别。)
普通优先级:100〜139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
实时优先级:0〜99(不关心)
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影
响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据
调整优先级
调整获得cpu资源的先后顺序,NI值越低,优先级越高,反之优先级越低、基于时间片的分时操作系统,考虑公平性,优先级可能会发生变化,但是变化幅度不会相差太大,默认80,每次修改都是从80开始操作nice范围是-20~19,linux进程优先级为60~99
用top命令更改已存在进程的nice:
- top
- 进入top后按“r”‒>输入进程PID‒>输入nice值
注意:
- 其他调整优先级的命令:nice,renice
- 系统函数:
让它加10,改完变成90,怎么还调低了呢,参考下文优先级队列
优先级设立不合理,会导致优先级低的进程长时间得不到cpu资源,进而导致:进程饥饿
补充概念-竞争、独立、并行、并发
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
进程切换
CPU上下文切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运行另外的任务时, 它保存正在运行任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中, 入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器,并开始下一个任务的运行, 这一过程就是context switch。
当进程要把自己的进程硬件上下文的数据报存下来,保存到哪里???
保存到tast_steuct里!!!
进程切换,最核心的,就是保存和恢复当前进程的硬件上下文的数据,及cpu寄存器的内容
linux源码
时间片:当代计算机都是分时操作系统,没有进程都有它合适的时间片(其实就是⼀个计数
器)。时间片到达,进程就被操作系统从CPU中剥离下来。
Linux2.6内核进程O(1)调度队列
从该结构中,选择⼀个最合适的进程,过程是怎么的呢?
1. 从0下表开始遍历queue[140]
2. 找到第⼀个非空队列,该队列必定为优先级最高的队列
3. 拿到选中队列的第⼀个进程,开始运行,调度完成!
4. 遍历queue[140]时间复杂度是常数!但还是太低效了!
- bitmap(unsigned int bitmap[5]):用位图二进制0表示没有,1表示该队列节点有进程链,方便快速遍历队列一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
- array[0],arry[1]:双队列中进行,若只有一个调度队列的话,只有上一个节点跑完,下一个节点才能得到资源,那这样的话,进程优先级低的长时间就得不到cpu资源防止进程饥饿
- 一个CPU拥有⼀个runqueue如果有多个CPU就要考虑进程个数的负载均衡问题
- 时间片还没有结束的所有进程都按照优先级放在该队列
- nr_active: 总共有多少个运行状态的进程
- queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
过期队列和活动队列结构一模一样
• 过期队列和活动队列结构模样
• 过期队列上放置的进程,都是时间片耗尽的进程
• 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针永远指向活动队列,expired指针永远指向过期队列
• 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直
都存在的。
•在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程
所以来新进程时链入expired队列里,等active队列跑完,然后和expired进行交换,循环往复。调整优先级也是链在expired里调整优先级,所以上文的+10,反而变慢了,在过期队列里进程优先级提高了,但是相比原来的就变慢了
总结:在系统当中查找一个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!