🏆个人主页:企鹅不叫的博客
🌈专栏
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
💙系列文章💙
【Linux】第三章Linux环境基础开发工具使用(yum+rzsz+vim+g++和gcc+gdb+make和Makefile+进度条+git)
文章目录
💎一、冯诺依曼体系结构
🏆1.冯诺依曼体系结构介绍
- 存储器: 对应的是我们电脑中的内存
- 中央处理器CPU: 其中由运算器和控制器两个部分构成
- 输入设备: 包括键盘、硬盘、鼠标等
- 输出设备: 硬盘、显示器等(输入设备和输出设备统称为外设)
结论:
- 外设并不是直接和CPU进行交互,而是先与内存进行交互,再与CPU进行交互,因为外设运行速度比较慢,CPU的运算速度是很快的,为了平衡二者之间的速度,会让CPU与介于二者运行速度之间的内存先进行交互
- 读入数据时,输入设备将数据写入到中介内存中,然后内存把数据写入到CPU中,让CPU进行数据的处理,处理完后,CPU将数据写回到内存中,最后由内存把数据写入到输出设备中
- 有了内存,CPU不需要和外设直接打交道
- 冯若依曼的原理是存储程序和程序控制
🏆2.为什么需要内存(存储器)
内存属于掉电易失性存储介质,也就是关闭计算机或者突然性、意外性关闭计算机的时候,里面的数据会丢失。
所有计算机数据传输到中央处理器(CPU)都是通过内存与中央处理器(CPU)进行传输处理的,由于外设的存储效率很低,所以CPU直接与外设进行数据交换速度很慢。而内存相比外设要快很多,但是比CPU又要慢。
在数据层面上,CPU能且只能对内存进行读写,不能访问外设;外设要输入或者输出数据,也只能写入内存或者从内存中读取。
这也就是为什么一个程序在运行起来的第一件事情是将程序加载到内存当中,因为程序(文件)是在硬盘(外设)上的,而CPU只能从内存当中获取数据,所以必须先将程序加载到内存,然后再从内存加载到CPU。
💎二、操作系统
概念和组成
概念: 管理计算机硬件与软件资源的计算机程序。(简称OS)
操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)其他程序(例如函数库,shell程序等等)。
设计目的
- 对上:为用户程序(应用程序)提供一个良好的执行环境
- 对下:做软硬件管理,提供稳定的软硬件环境
管理
操作系统主要进行以下四项管理:
- 内存管理:内存分配、内存共享、内存保护以及内存扩张等等。
- 驱动管理:对计算机设备驱动驱动程序的分类、更新、删除等操作。
- 文件管理:文件存储空间的管理、目录管理、文件操作管理以及文件保护等等。
- 进程管理:其工作主要是进程的调度。
先描述,再组织
- 描述: 在C/C++中,对一个对象进行描述一般是把这个对象的所有属性放在一个结构体或类中来进行描述,这样一遍我们更好地组织它们
- 组织: 用一些高效的数据结构来把这些对象组织起来,一般是链表、队列等一下高效的数据结构。
- OS管理的硬件部分: 网卡、硬盘等
- OS管理的软件部分: 内存管理、驱动管理、进程管理和文件管理,还有驱动和系统调用接口
系统调用和库函数概念
- 系统调用接口: OS不信任任何用户,会提供系统调用接口给用户提供服务,其中的细节我们不关心。(比如:我们平时写的printf函数要往显示屏上打印数据,这时候就要涉及硬件的访问,因为OS不信任任何用户,所以我们需要调用系统调用接口来完成,其中这个这个printf函数底层会帮我们调用需要用到的的系统调用接口来实现,帮程序员完成打印的操作)
- 系统调用: 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口
- 库函数: 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以有些开发者会对部分系统调用进行十度封装,这样就形成了库,方便上层用户使用
💎三、进程概念
概念
程序本质上是一个包含可执行代码的文件,是一个放在磁盘上的静态文件。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中,此时这个程序就被称为进程。所以进程就是一个开始执行但是还没有结束的程序的实例,是可执行文件的具体实现。当程序被系统调用到内存以后,系统会给程序分配一定的资源(内存、设备等)然后进行一系列的复杂操作,使程序变成进程以供系统调用。
进程是程序的一次执行过程和资源分配的基本单位
区分程序和进程
程序:程序本质是一个放在磁盘上的静态文件。
进程:程序运行起来之后,就叫做进程,进程是动态的,由操作系统管理。
🏆1.描述进程
构成
- 进程的所有属性信息都被放在一个叫做进程控制块的结构体中,可以理解为进程属性的集合。
- 这个数据结构的英文名称是PCB(process control block),在Linux的OS下的PCB是task_struct(Linux内核中的一种数据结构,它会被装载到RAM(内存)中并且包含并包含进程的信息)
进程创建
进程信息包括对应的文件+进程属性。进程属性是方便操作系统对进程进行管理,所以进程信息的大小要比原本的文件要大。创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插入到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效。
综上所述,进程就是可执行程序与管理进程需要的数据的集合。
task_struct
PCB的全称为Process Ctrl Block(进程控制块)。
Linux操作系统是用C语言进行编写的,因此Linux当中的PCB是由结构体实现的–
struct task_struct()
。
- 标示符: 描述本进程的唯一标示符,用来区别其他进程:Pid和PPid。
- 状态: 任务状态,退出代码,退出信号等。并不是所有的进程都是运行的,也有的进程是在等待运行,此时就需要状态信息。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 一个寄存器中存放了一个pc指针,程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据,在单核CPU中,进程需要在运行队列(run_queue) 中排队,等待CPU调度,每个进程在CPU中执行时间是在一个时间片内的,时间片到了,就要从CPU上下来,继续去运行队列中排队。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
🏆2.组织进程
在内核源代码中发现,所有运行在系统里的进程都以task_struct链表形式存在内核中。
🏆3.查看进程
显示全部进程
通过
ps
(process status)命令可以显示当前进程的状态,类似于 windows 的任务管理器。
- -e 列出所有的进程
- -f:全格式
- -h:不显示标题
- -l:长格式
- -w: 宽输出,显示加宽可以显示较多的资讯
- a:显示终端上的所有进程,包括其他用户的进程
- r:只显示正在运行的进程
- x:显示没有控制终端的进程
- u:试用用户格式输出
- j:采用工作控制的格式显示进程状况。
- l:采用详细的格式来显示进程状况。
比较常用的是ps ajx
显示某一进程信息
举例:现在有以下程序
#include<stdio.h> #include<unistd.h> int main () { while(1) { printf("hello\n"); sleep(1); } return 0; }
ps ajx | head -1 && ps ajx | grep main | grep -v grep
ps ajx | head -1为了显示第一行属性,&&表示执行完前面语句后执行后面语句,main在gcc时将mian.c重命名为main,
ps aux | grep mybin | grep -v grep
是为了过滤出mybin
程序并且屏蔽掉grep
这条命令本身。通过
kill PID
这条命令可以终止进程,例如 kill 4396
💎四、通过系统调用函数获取进程标识符-PID和PPID
- getpid: 获取进程id(PID)
- getppid: 获取父进程id(PPID)
- 记得包含头文件< sys/types.h > < unistd.h >
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { printf("pid:%d ppid:%d\n", getpid(), getppid()); return 0; }
注意: 普通进程的父进程基本都是bash,bash运行原理:bash叫做命令行解释器(在命令行下的所有命令几乎都是bash的子进程),bash只需要接受任务识别任务,然后创建子进程。通过创建子进程,让子进程去完成对应的任务。
💎五、通过系统调用获取进程标识符-fork(重点)
fork()函数
- 功能: 通过复制当前进程,为当前进程创建一个子进程
- 返回值: 有两个返回值,一个是父进程返回子进程的id,还有一个是子进程返回的0(fork失败返回值为-1)
举例:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { pid_t ret = fork(); if (ret < 0) { perror("fork"); return 1; } else if (ret == 0)// 子进程 { printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid()); sleep(1); } else if (ret > 0)// 父进程 { printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid()); sleep(1); } sleep(1); return 0; }
结果返回两个子进程和父亲进程轮流打印
补充
如何理解进程的创建?
创建进程,系统就会多一个进程,所以系统就要多一分管理进程的数据结构和该进程对应的代码和数据,父子进程代码共享,数据默认是一样的,但是当任一方试图写入数据,便以写时拷贝的方式各自一份副本,数据各自私有,具有独立性。
为什么fork有两个返回值?
在fork函数体内,函数返回id前已经完成了子进程的创建,既然完成了子进程的创建,那么子进程就也会去到运行队列中,等待CPU调度,父子进程共享代码,数据各自独立。由于返回值id是数据,所以虽然id的变量名相同,但是内存地址不同,所以返回的id是不同的,不同的返回值,让不同的进程执行不同的代码。
父子进程执行的顺序是怎样的?
这是不确定的,两个进程都在运行队列中等待CPU调度,由Linux下的调度器决定,所以这里是不确定的。
为什么父进程返回子进程pid,子进程返回0?
父进程必须有标识子进程的方案,fork之后,给父进程返回子进程的pid
💎六、运行状态
task_struct
结构体,该结构体内部有一个state状态码,用于标识当前进程处于什么状态
🏆1.进程运行
只要进入了运行队列的进程,就是
运行态
的进程,代表程序准备好了,可以随时调度
🏆2.进程终止
终止态:进程还在,但是永远不会运行,在队列中等待被释放。
为什么进程都终止了,不立马释放对应的资源?
- 这是因为当前CPU/操作系统正在忙着干其他的事情,没时间过来释放。所以会将不运行的进程放入终止态的队列(将该进程
task_struct
结构体插入该队列)
🏆3.进程阻塞
一个进程使用资源的时候,不仅仅会申请CPU的计算资源,还有可能申请其他更多的资源,如果申请这些资源的时候得不到满足,就需要排队
- CPU资源:运行队列
- 其他资源:也需要进行排队(磁盘,网卡)
当我们的进程访问某种资源,特别是外存(磁盘)这种慢设备资源的时候,如果磁盘暂时还没有准备好,操作系统就会把当前进程从运行队列剥离,插入到对应需要访问的设备下的等待队列中
🏆4.进程挂起
进程挂起和阻塞不同的是,阻塞只是单纯地在等待慢资源。而挂起则是该进程的数据被放入回了磁盘,进程本身依旧在排队等待。操作系统会有一个专门的
swap
分区,用来存放挂起进程
的代码和数据。
💎七、运行状态的描述
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 */ };
🏆1.R状态描述
R对应的是运行态,指进程在运行队列中,可随时被CPU调度,处于这个状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里等待被调度。
运行下列程序后,查看该进程状态,发现是S+(浅度休眠),因为程序在一瞬间就执行完了
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { while(1){ printf("%d\n",getppid()); sleep(1); } return 0; }
查询test文件进度的指令
ps jax | grep test
设置下列死循环即可进入运行态(R+)
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { while(1){ int a = 10; } return 0; }
🏆2.S状态描述
进程处于等待队列中,在等待时间完成,这里的睡眠是可以中断的,也叫浅睡眠
如果状态中有
+
则表示这个进程是在前台运行的,此时可以直接Ctrl+c
终止进程,如果在执行程序时加上&符:./proc &
,则表示将程序放到后台运行,此时无法直接使用Ctrl+c
终止进程,只能使用kill -9 PID
的方式。
🏆3.D状态描述
深度睡眠,处于D状态的进程不能被操作系统
kill
掉。要想杀掉一个D状态的进程,只有下面三种办法
- 等硬盘读写完毕,给进程返回结果之后,进程从D状态变成其他状态,操作系统进行处理
- 关机重启
- 拔掉电脑的电源
🏆4.T状态描述
T(stopped),暂停
kill 操作
//查看所有命令 kill -l
常用命令是第19和第18,分别用于
暂停/恢复
一个进程如果运行以下代码死循环,发现状态码是R+
#include <stdio.h> int main() { printf("start!\n"); while(1){ ; } return 0; }
执行以下代码(PID填写对应的标识符),该状态码会变成T,程序被暂停
kill -19 PID
此时执行以下代码(PID填写对应的标识符),进程会恢复成R,没有+,变成一个后台程序
kill -18 PID
此时,[ctrl]+C终止不了进程,只能 kill -9干掉进程
🏆5.t状态描述
t(跟踪状态):当进程被gdb调试的时候,会产生t状态,一般用于gdb调试打断点。
🏆6.X状态描述
死亡状态,这个状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,任务列表中是看不到的
🏆7.Z状态描述(僵尸进程)(重点)
概念
当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程,如果退出信息一直未被读取,则相关数据是不会被释放掉的。存在僵尸状态的原因是为了保持进程基本退出信息,方便父进程读取,获得退出原因。僵尸进程无法被杀死。
- 僵死的时候,task_struct是会被保留的,进程的退出信息是放在PCB中的
- 父进程没有读取子进程的状态信息,子进程就会进入僵死状态
- 父进程读取子进程状态码后,子进程会由Z状态变成X状态
下面是僵尸进程被系统删除后就在进程中找不到了
危害
- 进程的退出状态必须被维持下去,因为他要告诉诉其父进程相应的退出信息。可父进程如果一直不读取,那子进程就一直处于僵尸状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
- 如果一个父进程创建了很多子进程但是就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存。由于子进程申请的资源没有被回收,就会内存泄漏。
🏆8.前台进程和后台进程
如果状态中有
+
则表示这个进程是在前台运行的,此时可以直接Ctrl+c
终止进程,如果在执行程序时加上&符:./proc &
,则表示将程序放到后台运行,此时无法直接使用Ctrl+c
终止进程,只能使用kill -9 PID
的方式。
🏆9.孤儿进程(重点)
孤儿进程: 父进程先退出,子进程就称之为“孤儿进程”。孤儿进程会被1号systemed进程领养,被init进程领养,在后台运行。
如果进程没有父进程,且当前进程退出,那么当前进程进入僵死状态,该进程资源无法被回收造成内存泄漏,但是OS考虑了这个问题,孤儿进程是会被领养的运行以下代码之后,删除父进程,子进程会被系统接替
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> int main() { pid_t ret = fork(); if (ret < 0) { perror("fork"); return 1; } else if (ret == 0)// 子进程 { printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid()); } else if (ret > 0)// 父进程 { printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid()); sleep(10); exit(0); } sleep(1); return 0; }
💎八、进程优先级
🏆1.优先级相关概念
//查看bash下的进程 ps -la [Jungle@VM-20-8-centos:~]$ ps -la F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 1 S 1001 27546 1 0 80 0 - 1833 hrtime pts/0 00:00:00 test2 0 R 1001 27598 30493 0 80 0 - 38595 - pts/1 00:00:00 ps
🏆2.PRI和NI
- PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高,
- NI(nice)表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice,PRI(old)的值在Linux中默认为80,nice值默认为0,每一次设置的时候,
PRI(old)
都会被重置成80- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,范围[-20, 19],一共40个级别,所以PRI(new)的范围是[60,99]
🏆3.修改进程优先级
只有root用户才能提高进程的优先权(将nice值改为负数),其他用户需要使用sudo进行权限提升。
在使用
sudo top
后,进入界面按r
,输入需要设置的进程pid
后,再输入需要调整的nice值,之后按q即可退出