冯诺依曼体系结构
在计算机组成原理,我对此了深刻的讲解,有感兴趣的可以去看看,这里做简要概述。
- 存储器:对应的是我们电脑中的内存
- 中央处理器CPU:由运算器和控制器两部分组成
- 输入设备:包括显示器,磁盘,网卡,显卡,键盘,鼠标等。
- 输出设备:包括显示器,磁盘,网卡,显卡,音响等。
从这张图得到结论:
- 外设并不是直接和CPU进行交互,而是先与内存进行交互,再与CPU进行交互,因为外设运行速度比较慢,CPU的运算速度是很快的,为了平衡二者之间的速度,会让CPU与介于二者运行速度之间的内存先进行交互
- 读入数据时,输入设备将数据写入到中介内存中,然后内存把数据写入到CPU中,让CPU进行数据的处理,处理完后,CPU将数据写回到内存中,最后由内存把数据写入到输出设备中
- 有了内存,CPU不需要和外设直接打交道
- 冯若依曼的原理是存储程序和程序控制
操作系统
什么是操作系统
计算机系统的层次结构
- 操作系统(OS)是指控制和管理整个计算机系统的硬件和软件资源,并合理地组织调度计算机的工作和资源的分配,以提供给用户和其他软件方便的接口和环境,它是计算机系统中最基本的系统软件。
为什么要有操作系统
- 对上:给用户提供稳定,高效和安全的运行环境,为程序员提供各种基本功能(OS不信任任何用户,不让用户或程序员之间与硬件进行交互)
- 对下:管理好各种软硬件资源
如何管理
管理就是对需要管理的对象进行先描述,再组织这两个步骤的操作。
- 描述:在C中,假设想要知道一个学生的各门成绩,和获奖情况等,对一个对象进行描述一般是把这个对象所有的属性放在一个struct结构体中,这样让我们更方便的去组织使用它们。
- 组织:把这些对象组织起来,一般是链表,队列等高效的数据结构。
操作系统对软硬件管理
为了更好的理解这个表,举个最贴近的例子,比如:学校的校长,辅导员,学生。校长属于管理者,学生属于被管理者,辅导员属于执行者。 在校中我们除了开学和毕业可以看到校长之外,其他时间基本看不到,所以管理者和被管理者不是直接联系的,是通过辅导员这个执行者来进行传达一些信息和决策,就好比如:操作系统(管理者),底层硬件(被管理者),驱动程序(执行者)。这样对下管理好各种硬件资源我们就理解了。那么对上它又是怎么处理的?我们这里引出来几个概念:
系统调用和库函数的概念
- 系统调用接口:OS不信任任何用户,会提供系统调用接口给用户提供服务,其中的底层实现是不告诉我们的。(比如:我们平时写的printf函数要往显示屏上打印数据,这时候就要涉及硬件的访问,因为OS不信任任何用户,所以我们需要调用系统调用接口来完成,其中这个这个printf函数底层会帮我们调用需要用到的的系统调用接口来实现,帮程序员完成打印的操作)
- 系统调用:在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口。
- 库函数:系统调用在使用上,功能比较基础,对用户的要求也比较高,所以有些开发者会对部分系统调用进行封装,这样就形成了库,方便上层用户使用。
进程
基本概念
进程:计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
如果站在内核的角度来看:进程是分配系统资源的单位。
描述进程-进程控制块(PCB)
上面提到过一个抽象概念需要转化成一个具体的结构体来进行描述。进程中的信息就被放在一个叫做进程控制块(PCB)的结构体中。
在不同的操作系统下进程控制快名称不同,在Linux总描述进程的结构体叫做task_struct.
当一个程序被加载到内存中要开始执行的时候,操作系统同时会给该进程分配一个PCB,在Linux中就是task_struct这里面包含了所有关于进程的数据信息。所以CPU对task_struct进行管理就相当于对进程进行管理.
- 进程的所有属性信息都被放在一个叫做进程控制块的结构体中,可以理解为进程属性的集合。
- 这个数据结构的英文名称为PCB,在Linux的OS下的PCB是task_struct(Linux内核中的一种数据结构,它会被装载到RAM(内存)中并且包含并包含进程的信息)
简单语言描述进程
进程=程序文件内容+由操作系统自动创建的相关数据结构
task_struct内容分类
task_struct是Linux内核的一种数据结构,它会被装载到RAM里并包含进程的信息。每个进程都把它的信息放在task_struct这个数据结构里面,而task_struct包含以下内容:
- 标示符:描述本进程的唯一标识符,用来区别其他进程。
也就是进程的PID,PID是操作系统中唯一标识的进程号。有两个获得进程PID的方式:
1.可以使用ps axj查看进程的信息
2.可以使用系统接口得到进程PID和父进程PPID
#include <stdio.h>
#include <unistd.h>
int main() {
printf("pid=%d, ppid=%d\n", getpid(), getppid()); // 进程号和父进程号
return 0;
}
运行结果
- 状态:任务状态,退出代码,退出信号等(可以用$?查看上一次执行命令的退出代码)
- 优先级:程序被CPU执行的顺序(后面详细讲解)
- 程序计数器:一个寄存器中存放一个PC指针,这个指针永远指向即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 在单核CPU中,进程需要在运行队列中排队,等待CPU调度,每个进程在CPU中执行时间是在一个时间片内的,时间片到了,就要从CPU上下来,继续去运行队列中排队
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
- 记账信息:能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
组织进程
可以在内核源代码里找到task_struct
。所有运行在系统里的进程都以task_struct
双链表链表的形式存在内核里。如果在情况复杂的情况下,双链表中的节点也有可能存在在其他的数据结构中,例如队列等。
查看进程
查看进程三种方式:
- 进程的信息可以通过/proc系统文件夹查看;如:要获取PID为1的进程信息,你需要查看/proc/1这个文件夹
- 使用ps命令,ps aux:查看系统中所有的进程信息;ps axj:查看进程的父进程号;
- 使用top命令:动态查看进程的信息,其中的信息默认3秒更新一次
通过系统调用创建子进程
了解fork函数
- 功能:当使用fork()函数后,就在原来的进程中创建一个子进程,咋fork()之前的代码只被父进程执行,在fork()之后的代码有父子进程一起执行。
- 返回值:有两个返回值,一个是父进程返回子进程的id,另一个是子进程返回0
- 创建的子进程和父进程几乎一模一样,子进程和父进程的共享地址空间,子进程可以或者父进程中所有的文件,只有PID是父子进程最大不同。
先看一段代码:
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t pid=fork();
if(pid<0)
{
printf("error");
}
if(pid==0)
{
printf("i am a child process\n");
}
else
{
printf("i am a father process\n");
}
return 100 ;
}
代码运行结果:
在C/C++中,if-else语句指挥输出一条语句,但这里却打印了两句话,为什么?
这里父进程创建子进程成功后,两个进程具有独立性,会分别执行fork后面的代码,完成打印,所以有两条语句。
问题分析:
- 如何理解进程的创建?
创建进程,系统就会多一个进程,所以系统就要多一分管理进程的数据结构和该进程对应的代码和数据,父子进程代码共享,但不可以被修改,数据默认是一样的,但是当任一方试图写入数据,便以写时拷贝的方式各自一份副本,数据各自私有,具有独立性。
- 为什么fork有两个返回值?
在fork函数体内,函数返回id前已经完成了子进程的创建,既然完成了子进程的创建,那么子进程就也会去到运行队列中,等待CPU调度,父子进程共享代码,数据各自开空间。由于返回值id是数据,所以虽然id的变量名相同,但是内存地址不同,所以返回的id是不同的。
- 父子进程执行的顺序是怎样的?
这是不确定的,两个进程都在运行队列中等待CPU调度,由Linux下的调度器决定,所以这里是不确定的。
- 相同但独立的地址空间
两个进程其实地址空间是一样的,但是它们都有自己私有的地址空间,所以父子进程的运行都是独立的,一个进程中的内存不会影响另一个进程中的内存。
- 共享文件
子进程继承了父进程所有打开的文件,所以父进程调用fork的时候,stdout
文件呢是打开的,所以子进程中执行的内容也可以输出到屏幕上
进程状态
查看进程的状态
可以使用ps aux或者ps axj命令查看进程的状态
不同进程的状态
进程有很多的不同的状态,在kernel源代码中是这样定义的
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运行状态
这里并不是指进程一直在运行,而是指进程在运行队列中,可随时被CPU调度
模拟实现:
- S睡眠状态
这种状态是一种浅度睡眠,此时的进程是在被阻塞的状态中,等待着条件的满足过后进程才可以运行。在这种状态下可以被信号激活,也可以被信号杀死。
模拟实现:
可以使用sleep()
系统调用接口使得一个进程睡眠
#include <stdio.h>
int main()
{
while (1)
{
printf("hello world\n");
sleep(100); // 睡眠100秒
}
return 0;
}
- D磁盘休眠状态
这种状态是一种深度休眠状态,是不可以被大段的,在这种状态下即使是操作系统发送信号也不可以杀死进程,只能等待进程自动唤醒才可以。
模拟实现:
这种情况没法模拟,一般都是一个进程正在对IO这样的外设写入或者读取的时候,为了防止操作系统不小心杀掉这个进程,所以特地创建出一个状态保护这个进程。
总结:我们把从运行状态的task_struct放到等待队列中,就叫做挂起等待(阻塞),从等待队列放到运行队列,被COU调度就叫做唤醒进程!!!
- T停止状态
可以通过发送SIGSTOP信号让进程停下
模拟实现
kill -SIGSTOP PID // 停止进程
kill -SIGSONT PID // 继续进程
- x死亡状态
进程停止执行,进程不能投入运行。通常这种状态发生在接受到SIGSTOP、SIGTSTP、SIGTTIN、SIGOUT等信号的时候。
模拟实现
可以使用kill -9 PID
即可杀死一个进程
- Z僵死状态
后面详细讲解
孤儿进程
孤儿进程的概念
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
孤儿进程会产生危害嘛?
孤儿进程是没有父进程的进程,孤儿进程这个重任就落在了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出地子进程。这样,当一个孤儿进程凄凉地结束了其生命周期地时候,init进程就会代表党和政府出现处理它地一切善后工作,因此孤儿进程并不会有什么危害。
模拟实现:
模拟实现让父进程比子进程提前退出。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main()
{
pid_t pid;
//创建一个进程
pid = fork();
//创建失败
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//子进程
if (pid == 0)
{
printf("I am the child process.\n");
//输出进程ID和父进程ID
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("I will sleep five seconds.\n");
//睡眠5s,保证父进程先退出
sleep(5);
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("child process is exited.\n");
}
//父进程
else
{
printf("I am father process.\n");
//父进程睡眠1s,保证子进程输出进程id
sleep(1);
printf("father process is exited.\n");
}
return 0;
}
运行结果:
父进程退出后,子进程被1号init进程收养。
僵尸进程
为什么会出现僵尸进程?
任何一个子进程(除init外)在exit()之后,并非马上消失,而是留下一个称为僵尸进程的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit之后,父进程并没有来得及处理,这时用ps命令就能看到子进程的状态时"Z“。如果父进程能及时处理,可能用PS命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态.如果父进程在子进程结束之前退出,则子进程由init接管。
僵尸进程的概念
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程地状态信息,那么子进程地进描述符仍然保存在系统种,这种进程称之为僵死进程。
僵尸进程的危害
进程已经结束了,但是进程控制块PCB却还是没有被释放,这时就会浪费这一块资源空间。所以会导致操作系统的内存泄漏。
如何消灭僵尸进程?
例如有个进程,它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就推出了,因此这个子进程的生命周期很短,但是父进程只管生成新的子进程,至于子进程退出之后的事,则一概不问,这样系统运行上一段时间,系统就会存在很多僵死进程。倘若用ps命令查看的话,就会看到很多状态为Z的进程。严格来说,僵死进程并不是问题的根源,罪魁祸首是产生大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号)。枪毙元凶进程后,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放他们占用的系统进程表中的资源。
模拟实现:
模拟实现让子进程比父进程提前退出。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == 0) {
int count = 5; // 子进程运行5次
while (count --) {
printf("I am a child, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
} else { // 父进程一直运行
while (1) {
printf("I am a father, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
可以使用shell脚本监控
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep; sleep 1; echo "############"; done
运行结果
进程状态转化
进程优先级
进程优先级的概念
进程优先级的定义
进程优先级为进程获取CPU资源分配的先后顺序,即进程的优先权,优先级高的进程可以优先执行的权力。
进程优先级的意义
之所以会存在进程优先级,是因为
cpu
本身的资源分配是有限的,一个cpu
一次只能run
一个进程,但是一个操作系统中可能会有成千上百的进程,所以需要存在进程优先级来确定每一个进程获得cpu
资源分配的顺序。
查看进程优先级
在linux或者unix系统中,用ps -al或者ps -l命令则会类似输出以下几个内容:
其中我们来了解几组关于进程优先级的相关信息
- UID:执行者的身份,用户标识符
- PID:进程的编号
- PPID:进程的父进程的编号
- PRI:进程可被执行的优先级,PRI越小代表优先级越高
- NI:进程的nice值,代表进程优先级的修改数值
PRI和NI
PRI和NI是一组对应的概念。NI的取值会影响到PRI的最终值。
PRI代表进程被CPU执行的先后顺序,并且PRI越小进程的优先级越高。NI代表nice值,表示进程的优先级的修改数值。所以两者之间有一个计算公式:(new)PRI=(old)PRI+NI。
注意:
- PRI在系统中默认初始化为80
- NI的取值范围为-20~19,一共40个级别
- 当NI值为负值,那么该程序将会优先级值变小,即其优先级会变高,则其越快被执行。
例如:
默认进程的PRI为80,当前的nice值为10,所以最终的PRI为90.
总结:在linux环境下,我们一般说调整进程的优先级,就是在调整nice值。nice值决定性的影响到进程优先级。
更改nice值
通过top命令更改nice值
- 使用
top
命令后,按r
键,要求你输入需要更改进程优先级的进程PID - 输入需要更改进程优先级的进程PID
- 输入你想要更改后的
nice
值,按回车键即可
当然还有很多方法去更改nice值,这里不做过多讲解,感兴趣的同学可以去查查相关知识。
进程相关概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
- 独立性:多个进程运行,需要独享各种资源,多个进程运行期间互不干扰。
- 并发:多个进程在一个
CPU
下采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发,所以两个并发的进程之间在执行之间上有重叠的部分。- 并行:多个进程在多个
CPU
下同时运行,称之为并行。
今天就到这里啦,如果对你有帮助的话,那就一赞三连吧,你的支持就是我持续更新的动力,爱你吆!!!