🌈个人主页:Yui_
🌈Linux专栏:Linux
🌈C语言笔记专栏:C语言笔记
🌈数据结构专栏:数据结构
🌈C++专栏:C++
文章目录
1. 进程
1.1 基本概念
当我们编写完代码运行并让代码编译起来时,在当前路径下会出现由代码编译而成的可执行文件.exe。当我们运行这个可执行文件时,该程序便会被加载到计算机的内存当中,一些教材可能会把这个已经运行起来的程序叫做进程,其实这是不完整的,真实的进程还需要包括管理这个程序的PCB。
- 课本概念:程序的一个执行实例,正在执行的程序。
- 内核观念:担当分配系统资源(CPU时间,内存)的实体。
1.2 描述进程 -PCB
- 进程信息被放在一个叫做进程控制块(process control block)的数据结构中,可以理解为进程属性的集合。
- 在教材中一般叫PCB(process control block),Linux操作系统下的PCB是
task_struct
.
PCB是一个结构体,是为了管理加载到内存的程序而产生的。
1.2.3 介绍task_struct
在Linux中每一个进程都由task_struct数据结构来定义,task_struct就是我们通常所说的PCB,它是队进程的唯一控制手段也是最有效的手段,当我们调用fork()时,系统会为我们产生一个task_struct结构。然后从父进程继承一些数据,并把新的进程插入到进程树,以待进行进程管理,因此了解task_struct的结构对于我们理解进程调度的关键。
task_struct是如何管理进程的,先描述,再组织。在task_struct结构中有以下定义:
- 进程状态,将记录进程在等待、运行、或者死锁。
- 调度信息,由哪个调度函数调度,怎样调度等。
- 进程的通讯情况。
- 因为要插入进程树,必须有联系父子兄弟的指针,当然是task_struct类型。
- 时间信息,比如计算好执行时间,以便于CPU分配。
- 标号,决定计进程归属。
- 可以读写打开的一些文件信息。
- 进程上下文和内核上下文。
- 处理上下文。
- 内存信息。
- 等等
因为每个PCB都是这样的,只有这些结构才能满足另一个进程的所有要求。
task_struct:
struct task_struct
{
//标识符:描述本进程的唯一标识符,用来区分其他进程
//状态:任务状态,退出代码,退出信号
//优先级:相对于其他进程的优先级
//程序计数器:程序中即将被执行的下一条指令的地址。
//内存指针:包括程序代码的进程相关数据的指针,还有其他进程共享的内存块的指针。
//上下文数据:进程执行时处理的寄存器中的数据。
//I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
//记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,计账号等。
//...
};
具体结构看:task_struct结构体结构
提问:为什么程序加载到内存中,变成进程之后,我们要给进程形成一个PCB对象呢?
回答:因为操作系统需要进行管理,操作系统是无法直接对正在运行的程序做管理的,需要通过一个PCB来间接管理,PCB上有进程的信息,对PCB对象的管理其实就是对进程的管理。
所以:进程 = 内核PCB对象 + 正在运行的程序
这就是说,所有对进程的控制和操作,都只和进程PCB有关,和进程的可执行程序无关!如果你想,你可以把PCB放到任何数据结构当中。
1.3 查看进程
进程信息可以通过/proc
系统文件中查看。
-
当你需要获取PID为1的进程信息,你需要查看
/proc/1
这个文件夹。
-
大多数进程信息同样可以使用top和ps这些用户级工具来获取。
ps aux | grep test | grep -v grep
过滤出于test有关的进程
1.4 通过系统调用获取进程标识符
- 进程id(PID)
- 父进程id(PPID)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = getpid();//获取该程序进程标识符id
pid_t fid = getppid();//获取该程序进程父进程的标识符id
while(1)
{
printf("i am process! pid = %d,ppid = %d\n",id,fid);
sleep(1);
}
return 0;
}
pid_t也是一个类型,就像int一样,int用来标识整型,pid_t就用来标识进程号类型。
如何判断确实是这样呢?
while :;do ps ajx|head -1 && ps ajx|grep mybin|grep -v grep;sleep 1; done
使用该条语句可以在屏幕中循环打印mybin
的进程是否存在。
1.5 通过系统调用创建进程 -fork
- 运行
man fork
认识fork - fork有两个放回值。
- 父子进程代码共享,数据各开辟空间,私有一份(采用写时拷贝)
功能
fork是复制进程的函数,程序一开始就会产生一个进程,当这个进程(代码)执行到fork()时,fork就会复制一份原来的进程来产生一个新的进程,新产生的进程为子进程,而原来的进程为父进程,此时父子进程是共存的,他们会同时向下执行代码。
关于放回值
在父进程中,fork会返回新创建子进程的进程ID,在子进程中,fork返回0。如果出现错误,fork会返回一个负值。
也就是说,在fork函数执行完毕后,如果创建进程成功,则出现两个进程,一个子进程,一个父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建的子进程的进程ID。我们可以通过dork返回的值来判断当前进程是子进程还是父进程。
下面写一段代码,执行逻辑将会和过去不同。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("出错!!!\n");
return 0;
}
if(id == 0)//子进程
{
printf("i am a precoss PID:%d PPID:%d\n",getpid(),getppid());
}
else
{
printf("i am a precoss PID:%d PPID:%d\n",getpid(),getppid());
}
}
return 0;
}
//打印结果
/*
i am a precoss PID:29088 PPID:19144
i am a precoss PID:29089 PPID:29088
*/
运行结果:
简直匪夷所思!if里的内容和else里面的内容居然同时执行了。这是怎么回事呢?
就像前面所说,在fork函数执行完毕后,如果创建进程成功,则出现两个进程,一个子进程,一个父进程。子进程和父进程是同时运行的,可以看出两个程序,不过他们的代码数据是相同的。由由于fork在父子进程中的返回值不同,也就造成了这种看上去if和else同时执行的情况。
2. 进程状态
- 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有多个状态,在Linux内核中,进程也可以叫做任务。
所谓的状态就是一个整型变量,在task_struct中的一个整型变量。
下面是进程状态在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.
*/
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 */
};
提问:为什么要有进程状态呢?
回答:在日常生活中,可能你感冒了,你会对室友说,我今天状态不好就不去上课了。这里的状态就决定了你的后续动作——不去上课了。在Linux中也是如此,Linux可能存在很多的进程,操作系统要根据它们的状态来决定后续对这些进程的操作。
2.1 通俗的5种状态
进程的状态,通俗的来讲有5种:创建状态、就绪状态、堵塞状态、执行状态、终止状态。
最基本的状态就是:运行状态、就绪状态、堵塞状态。
- 就绪状态:进程已经具备运行条件,但是由于没有空闲的CPU,而暂时不能运行。
- 运行状态:进程正在运行,占用CPU资源。
- 堵塞状态:因为正在等待某一事件而暂时不能运行,如程序正在执行sleep,或者等待输入。
创建态与结束态 - 创建态:进程正在被创建,操作系统为分配资源、初始化PCB
- 进程终止从系统中撤销,操作系统会回收进程拥有的资源。
2.2 进程具体的状态
上面的状态好像和前面我们所写的状态不太一样啊,确实,在前面我们所写为的为进程具体的状态,相当于通俗状态的具体实例。
再来看看代码:
/*
* 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):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠)。对于了堵塞状态。
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止进程,这个被暂停的进程可以发送SIGCONT信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
说了这么多,有没有可信度呢?当然了下面就是查看进程状态。
2.3 进程状态的查看
ps aux / ps axj 命令
有什么区别呢?
aux,axj其实是分开看的:a-u-x-j
- ps a显示现行终端机下的所有程序,包括其他用户的程序。
- ps u以用户为主的格式来显示程序状态。
- ps x显示所有程序,不以终端机来区分。
- ps j显示进程归属的进程组id、会话id、父进程id
也就是说,ps aux就是以用户为主打印所有进程。ps axj打印所有进程并显示pid、ppid。
先写一个程序让它跑起来,然后观察它的状态。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("I am a process!\n");
sleep(1);//休眠一秒
}
return 0;
}
makefile的配置:
mybin:test1.c
gcc -o mybin test1.c
.PHONY:clean
clean:
rm -f mybin
把xshell开个双窗口,来观察进程的状态,一个用来运行程序,一个用来观察状态。
在此之前,我们还要在写一个循环擦查看进程状态的指令。
while :;do ps ajx|head -1 && ps ajx|grep mybin|grep -v grep;sleep 1; done
每秒打印一次mybin进程的状态。下面让我们运行一下程序来看看吧。
从该程序我们可以看出该进程的各个信息,其中有个信息是有关进程状态的。就是STAT。
可是在上面的图片中为什么我们进程显示的状态是S呢?S可是睡眠的意思啊。令人费解,程序不是正在运行吗?其实图片没有错的,在我们的程序中存在一个sleep函数会让程序休眠一秒钟,进程不能在它睡眠期间还把它放在运行状态,这也就是导致了,mybin
的运行状态只有一瞬间,运气好的话可能能捕捉到这一瞬间。
那么思考一下,如果把sleep去掉,STAT
会是什么状态呢?
答案还是S
,这是因为cout
的缘故,printf
也会存在休眠时间的,CPU的速度是非常快的,printf
的休眠时间对于CPU的速度来说就显得非常漫长了,所以STAT
依旧会显示S
。
那么如何才能显示R
呢?把printf
去掉就可以,直接让程序执行死循环。
就是如此。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int a = 0;
while(1)
{
scanf("%d",&a);
}
return 0;
}
2.4 介绍僵尸进程与孤儿进程
Z(zombie)-僵尸进程
- 僵尸状态(Zombies)是一个比较特殊的状态,当进程退出并且父进程没有读取到子进程退出的返回代码就会产生僵尸进程。
- 僵尸进程会终止状态保持在进程表中,并且会一直等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但是父进程没有读取子进程的状态,子进程进入僵尸状态。
创建一个维持30秒的僵尸进程:
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id<0)
{
//程序出错
perror("fork");
return 1;
}
else if(id > 0)
{
printf("parent[%d] is sleeping...\n",getpid());
sleep(30);
}
else
{
printf("child[%d] is begin Z...\n",getpid());
sleep(5);
exit(-1);
}
return 0;
}
僵尸进程的危害
- 进程的退出状态必须维持下去,因为它要告诉关心它的进程(父进程),父进程的任务,我完成的如何。可是父进程如果一直不读取,那子进程就一直处于Z状态。
- 维护退出状态本身就是要用数据维护,也属于进程的基本信息,所以保存task_struct(PCB)中,Z状态一直不退出,PCB将会一直维护它,不退出。
- 那父进程创建了很多子进程,就是不回收,就会导致内存的资源的浪费。因为数据结构对象就要占用内存。
- 僵尸进程会导致内存泄漏!
孤儿进程
- 父进程如果提前退出,那么子进程后退出,进入Z之后如何处理。
- 父进程先退出,子进程就称为“孤儿进程”
- 孤儿进程会被1号进程收养。
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id<0)
{
//程序出错
perror("fork");
return 1;
}
else if(id > 0)
{
printf("parent[%d] is sleeping...\n",getpid());
sleep(3);
exit(-1);
}
else
{
printf("child[%d] is begin Z...\n",getpid());
sleep(10);
}
return 0;
}
可以看到,子进程被1号进程接管了。
3.进程优先级
3.1 为什么要有优先级的概念
大多数人的电脑都是一个CPU,一次只能处理一个进程任务,但是进程又有很多个。这也就导致的CPU的资源不足,为了更合理的利用CPU资源,就存在进程优先级来确定进程获取CPU资源的顺序。
就要生活中的排队,进程在CPU中也是需要排队的,除了遵循先来后到的排队原理,还存在优先级更高的进程是可以进行插队的,这也可以理解,在医院排队时,如果碰到急诊病人是可以优先挂号的。
提问:有没有可能因为大量的优先级更高的进程插队导致低优先级的进程迟迟得不到执行。
回答:这就涉及到了进程饥饿的问题了,在Linux下是有相关解决方法的,Linux会维护两个队列,一个为活跃队列,另一个为过期队列,这里就不细讲了。
3.2 进程优先级的基本概念
- CPU资源分配的先后顺序,就是指进程的优先级(priority)
- 优先权高的进程有优先执行权力。配置进程优先级对多任务环境的Linux很有用,可以改善系统的性能。
- 把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
进程要访问某种资源,就必须通过一定的方式,确定享受资源的先后顺序。
可能有人会把进程的优先级和权限进行类比,其实两者还是很不同的:
优先级决定的是顺序问题。
权限决定的能不能的问题。
3.3 查看系统进程
在Linux或者unix系统中,输入ps -l
会出现以下内容:
下面我们来介绍各个符号的意义
- UID:代表执行者的身份。
- PID:代表这个进程的代号。
- PPID:代表这个进程是由哪个进程发展衍生而来的,也就是父进程的代号。
- PRI:代表这个进程可被执行的优先级,其值越小优先级越高。
- NI:代表这个进程的nice值。
3.3.1 介绍PRI和NI
- PRI就是进程的优先级,通俗点就是程序被CPU执行的先后顺序,此值越小进程的优先级越高。优先值范围[60,99],一般默认的优先级为80
- 那NI就是我们要说明的nice值,表示进程可被执行的优先级的修正数值。
- PRI越小越快被执行,那么加入nice值后,就会得到新的PRI:PRI(NEW) = PRI(OLD)+nice.
如此一来的话,当nice值为负值的时候,那么该程序就会优先值变小,其优先级会变高。则越快被执行。
nice的取值范围为[-20,19],一共40个级别,这也就对于了第一点。
3.1.2 调整进程的优先级
通过公式PRI(NEW) = PRI(OLD)+nice。我们知道,调整进程的优先级的本质就是调整nice的值。
指令:top
进入top后按“r”->输入进程PID->输入nice值
演示:修改niec值为100.
通过演示我们可以发现,尽管nice的值被修改为100,PRI的值就值变成了99.由此也可以证明PRI的范围。
值得注意的是,每次都是直接给nice赋值,而不是在原有nice的基础上加减。
提问:为什么调整优先级是要受限制的?
回答:如果不加限制,将自己进程的优先级调整的非常高,别人的优先级调整的非常低,优先级较高的进程,优先得到资源,后续源源不断的进程产生。常规进程很难享受到CPU的资源,也就造成的进程饥饿问题。
3.4 Linux的调度与切换
提问:在进程运行时,CPU会直接把进程代码跑完吗?
答案:是不会的,现代操作系统就是基于时间篇进行轮流执行的,假设每个进程执行1ms,那么CPU在每1ms都会切换进程来执行。
一些其他概念
- 竞争性:系统进程数目众多而CPU资源只有少量,甚至一个,所以进程间是具有竞争属性的,为了高效完成任务,更合理竞争相关资源,便有了优先级。
- 独立性:多进程运行,需要独享各种资源,多进程运行期间,互不干扰。
- 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。
- 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程得以推进,称之为并发。