一、冯·诺依曼体系结构(硬件方面)
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
1、冯·诺依曼体系结构的存储器指内存,掉电易失。
2、而磁盘是一种外存,可以永久性存储数据。磁盘也属于外设的一种。外设又分为输入设备和输出设备。磁盘和网卡既是输入设备又是输出设备。
3、对于中央处理器,它有自己的指令集,外部程序翻译为CPU的指令集,让CPU根据这些指令集去执行。
4、为了保证读取和写入速度,CPU只和内存打交道。
举个例子加深理解
现在我用我的电脑向我朋友的电脑传输一份压缩包文件。根据冯·诺依曼体系结构,将会经过以下流程:
二、操作系统(软件方面)
操作系统是管理软硬件的软件。
1、操作系统的理解
1、操作系统通过持续获取数据,进行软硬件的管理;
2、操作系统的管理的方法是先描述,再组织。先对被管理对象进行抽象成类,再根据这个类定义一个个具体的对象,再将这些对象通过数据结构进行关联,将软硬件的管理转换成对数据结构的管理。这其实也是面向对象的思想。
3、数据的采集和操作系统命令的下达,由驱动来做。
2、系统调用和库函数的概念
系统调用:从开发的角度,操作系统对外会表现为一个整体,但会暴露部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
库函数:用户直接使用系统调用很困难,所以有了对系统调用进行封装的库函数。
三、进程控制块PCB的概念
1、我们写的可执行程序并不包含任何进程的属性信息。这些进程信息被放在一个叫做进程控制块的结构体中,用数据结构形成关联,可以理解为进程属性的集合;
2、该结构体被称之为PCB(process control block),Linux操作系统下的PCB是: task_struct;
四、进程的概念
1、什么是进程?
进程=内核数据结构(task_struct)+进程对应的磁盘代码和数据。
进程在调度运行的时候,就具有动态属性。
2、进程如何管理?
先描述,再组织。
操作系统会给每一个进程创建一个PCB对象,这些进程控制块对象用链表形成连接,通过遍历PCB的方式来找到对应状态的进程进行执行。例如找到优先级最高的PCB所对应的进程进行执行或通过PCB找到已死亡的进程进行释放。
通过这种方式,操作系统对进程的管理就变成了对进程所对应的PCB的管理,即链表的增删查改!
3、进程相关指令
3.1查看进程
ps axj | grep 'myproc'
能够显示出所有有关myproc的进程信息。
ps axj | head -1 && ps axj | grep 'myproc'
这个指令可以带上进程的小标题。
3.2杀掉进程
kill -9 8833
这里的8833是目标进程的PID。
3.3查看/proc文件系统
ll /proc/
/proc目录存在于内存中,这些数字名字的目录就是进程对应的PID。
ll /proc/14456 -d
能够找到正在运行中的PID14456的进程。
如果进程在运行时,可执行文件被删除,进程不会停止,但是进程目录中该可执行程序的路径将会无效。
4、系统调用
4.1子进程getpid()
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("hello world,MyPID=%d\n",getpid());
sleep(1);
}
return 0;
}
调用getpid函数后,会将进程的PID打印出来
4.2父进程getppid()
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("hello world,MyPID=%d,MyPPID=%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
可以发现,这个进程的父进程就是bash。命令行上启动的程序,一般它 的父进程没有特殊情况的话,都是bash。bash通过派生子进程的方式执行程序,如果程序有bug退出了,那挂掉的仅仅是子进程,bash没有影响。
4.3创建子进程fork()
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id=fork();
printf("MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid(),id);
return 0;
}
这个代码的执行结果是两个打印。
再看:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//子进程
while(1)
{
printf("子进程,MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else if(id>0)
{
//父进程
while(1)
{
printf("父进程,MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else
{
}
return 0;
}
父子进程不断循环。说明fork()之后会有父子两个进程,fork()之后的代码,父子进程共享。
五、进程状态的理解
进程在不同的队列中,表示不同的状态。
1、什么是运行队列
1个CPU只有一个运行队列,进程的执行需要排队。让进程入队列,本质上是将task_struct结构体对象放入运行队列中。
这个进程队列中的指针可以找到需要加载的进程,将其PCB对象的信息加载到CPU中,CPU可以根据PCB对象的信息找到进程对应的代码进行执行。
在CPU的运行队列中的进程状态就叫做运行状态。
状态,是进程的内部属性,存放于task_struct中。这个状态可以理解为一个整数,例如这个整数为1代表运行;2代表停止,3代表死亡状态等······
2、对于CPU和硬件的速度差异,系统如何调度?
进程不仅仅会占用CPU资源,也会占用硬件资源。对于CPU,它可以很快的处理进程的请求;但是对于硬件,速度很慢,例如网卡,可能同时有迅雷、百度网盘、QQ等进程需要获取网卡的资源,所以每一个描述硬件的结构体中也有一个task_struct* queue运行队列指针,指向排队中的PCB对象的头结点。
那么CPU和硬件的速度差异巨大,系统该怎么平衡这种速度?当CPU发现运行状态的进程需要访问硬件资源时,会让该进程去所需访问的硬件的运行队列中排队,CPU继续执行下一个进程。
那么这个被CPU剥离至硬件运行队列中的进程状态被称为阻塞状态。当进程对硬件的访问结束后,进程的状态将会被修改为运行状态,即该进程重新回到CPU的运行队列。
3、对于过多的阻塞进程,内存占用如何处理?
硬件的速度较慢,但是大量的进程需要访问硬件,势必会产生较多的阻塞进程,这些阻塞进程的代码和数据在短期内不会被执行,如果全部存在于内存中将会导致内存占用。
对于这个问题,如果内存中有过多的阻塞状态的进程导致内存不足,操作系统会将其的代码和数据先挪动至磁盘,仅留PCB结构体,以节省内存空间,这种进程状态被称为挂起状态。将进程相关数据,加载或保存至磁盘的过程,称为内存数据的换入和换出。
进程的阻塞状态不一定是挂起状态,部分操作系统可能会存在新建状态挂起或运行状态挂起等。
六、Linux操作系统的进程状态
1、进程状态在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 */
};
2、进程的运行状态
#include <stdio.h>
int main()
{
while(1);
return 0;
}
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
状态后面有+号的表示前台进程,没有+号表示后台进程。前台进程在执行时,用户无法继续输入指令除非ctrl+c终止程序;后台进程在执行的过程中,用户可以输入指令且ctrl+c无法杀掉该进程。可以使用kill -9 PID的指令杀掉该进程。
3、进程的浅度睡眠状态(阻塞状态的一种)
浅度睡眠状态是可以被终止的进程状态。
#include <stdio.h>
int main()
{
int a=0;
while(1)
{
printf("%d\n",a++);
}
return 0;
}
虽然数值一直在打印,但是printf函数需要访问显示器,大部分时间在等显示器IO就绪,只有小部分时间在执行打印代码。所以该代码呈现睡眠状态。
需要访问外设的,一般属于睡眠状态。
**S睡眠状态(sleeping)**:意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 【interruptible sleep】)。
那么有人会问了,为什么我的grep进程也处于S状态?博主试过多次执行该指令,grep进程有小概率会处于R状态,其实这个和他所查询的进程的状态也有关系,我在查询的进程在摸鱼,那我也去摸会鱼吧~
4、进程的深度睡眠状态(磁盘休眠状态)(阻塞状态的一种)
只有在高IO的情况下才会发生(Linux中有一个dd命令可以模拟高IO的状态,可以动手试逝)。
D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的
进程通常会等待IO的结束。
在该状态下的进程,无法被操作系统杀掉,只能通过断电或者进程自己醒来的方式中断深度睡眠状态。
5、进程的暂停状态/追踪暂停状态(阻塞状态的一种)
可以输入kill -19 PID来让一个进程进入暂停状态。或者将一个任务从前台切换到后台等,进程均会变为暂停状态。
T暂停状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。(kill -18 PID)
我们在使用gdb对一个可执行文件调试的时候,程序运行到断点处,程序会进入**t追踪暂停状态(tracing stop)**:表示该进程正在被追踪。
6、进程的僵尸状态/死亡状态
**Z僵尸状态(zombie)**:为什么会存在僵尸状态?因为进程在退出的时候,不会立即释放该进程对应的资源,会保存一段时间,让父进程或操作系统来读取子进程的返回代码。那么进程怎样进入僵尸状态?
模拟子进程正常退出,父进程不回收子进程(不读取子进程的返回信息)的场景(也可以使用kill -9 PID杀掉子进程):
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//子进程
while(1)
{
printf("子进程,MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid(),id);
sleep(1);
exit(1);
}
}
else if(id>0)
{
//父进程
while(1)
{
printf("父进程,MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else
{
perror("fork");
exit(-1);
}
return 0;
}
通过该命令循环打印进程信息,发现子进程已经变成了僵尸状态。旁边的<defunct>译为死者,进程已经死亡,但是还未被回收,这就是僵尸状态。
僵尸进程的退出结果会写在PCB中,一个进程退出了,它的代码和数据会被释放,但是它的PCB是不会被释放,如果父进程不回收这块资源,那么会造成系统的内存泄漏。那我能不能手动杀掉这个僵尸进程来手动释放僵尸资源?不可以,因为僵尸进程已经死亡,无法手动杀掉进程。
在Z状态的进程被回收后,进程状态变为X死亡状态(dead):父进程读取完子进程的返回信息后,收尸速度太快了,我们看不到,进程死亡状态立马被它的父进程回收。
7、孤儿进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//子进程
while(1)
{
printf("子进程,MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else if(id>0)
{
//父进程
while(1)
{
printf("父进程,MyPID=%d,MyPPID=%d,%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else
{
perror("fork");
exit(-1);
}
return 0;
}
前台创建的子进程,父进程被杀掉后,父进程立马被bash回收。子进程被1号进程(操作系统)领养,同时切换到后台运行(使用kill -9 PID杀掉后台进程)。因为不领养子进程的话,那么子进程退出时呈现的僵尸状态就没有谁能回收了(内存泄漏)。这种被领养的进程称为孤儿进程。
七、进程的优先级
1、进程优先级的概念
进程的优先级本质就是PCB中的一个整数数字(不同操作系统可能由多个数字决定)。
使用ps -la命令显示出当前的进程信息。PRI(priority)代表优先级的意思(priority默认是80);NI(nice)用于调整优先级(nice默认是0)。
进程最终优先级=默认优先级(固定80)+nice值。
Linux支持进程在运行过程中调整优先级,调整的方式是修改nice值。
2、nice值的修改
注:1、需要root权限2、使用r调出修改nice值的命令栏。
nice的取值范围为【-20,19】,数字输的再小再大也没用。
所以Linux中进程的权限范围为【80-20,80+19】,数字越小,优先级越高。
进程优先级不要人为的调整,如果一个进程的优先级较高或较低,可能会造成其他进程获取操作系统资源不均,造成操作系统自身的调度失衡。
八、进程切换
1、进程特性
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级 。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰 。
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行 。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
2、进程切换(并发)
cpu中有一个eip寄存器(PC指针),指向下一条指令的地址。
进程在运行的时候,占有CPU,会产生很多的临时数据,归属于当前进程。虽然CPU内部仅有一套寄存器硬件,但是寄存器中保存的数据属于当前进程。(寄存器是共享的,但是数据是各进程私有的)
进程在运行的时候都有自己的时间片,这个时间一到,即使进程还没有被执行完毕,但是会被操作系统剥离CPU,腾出CPU让下一个进程上来跑一跑。
那么这个进程下次再回到CPU继续运行时,操作系统是如何知道这个进程的代码被执行到哪里了?
首先,进程在切换的时候,需要进行上下文保护,一些临时数据被保存至PCB里(误);进程在恢复运行的时候,要进行上下文的恢复,后续该进程回到CPU运行时,将加载这些数据。通过PC指针继续运行下一行代码。
版权归原作者 蒋灵瑜的笔记本 所有, 如有侵权,请联系我们删除。