🌠专栏:Linux
目录
一、冯诺依曼体系结构
🌟系统调用和库函数概念
输入设备:键盘、鼠标、网卡、磁盘(外存)......
输出设备:显示器、磁盘、网卡......
存储器:内存
CPU(运算器+控制器):可以直接向外设发送控制信息
1、CPU在数据层面,不会和外设直接打交道,只会和内存进行交互;
例:二进制程序 --> 文件 --> 磁盘 --> 外设。
任何程序,运行的时候,都必须先被(从磁盘)加载到内存(由体系结构决定的!)。
2、内存是 CPU和外设之间的一个巨大缓存!
3、当数据在计算机内部流转的时候,本质是在不同的设备间进行拷贝!!设备的拷贝效率本质就是计算机的效率。
二、操作系统OS
OS的本质其实是一种,进行软硬件资源 管理 的软件。
为什么要有 OS ?
1、操作系统对下软硬件资源的管理,稳定的、高效的、安全的,能进行良好的工作(手段);
2、操作系统对上要给用户提供一个稳定的、高效的、安全的运行环境(目的)。
设计OS的目的:
与硬件交互,管理所有的软硬件资源;
为用户程序(应用程序)提供一个良好的执行环境;
操作系统的定位是:一款纯正的“搞管理”的软件;
三、进程
我们在编写完代码并运行起来时,在我们的磁盘中会形成一个可执行文件,当我们双击这个可执行文件时(程序时),这个程序会加载到内存中,此时,这个程序就为进程。即只要把程序(运行起来)加载到内存中,就称为进程。
进程的概念:程序的一个执行实例,正在执行的程序等。
在内核的角度上看:担当分配系统资源的实体。
描述进程-PCB:
• 一个概念需要一个具体的结构体来进行描述。进程中的信息就被放在一个叫程序控制块(PCB)的数据结构体中。即进程属性的集合。
• Linux操作系统下的PCB为 task_struct
• task_struct 是Linux内核的一种数据结构,它会被操作系统在内存中进行创建,里面包含着进程相关的管理信息。
task_ struct内容分类 :
• 标示符: 描述本进程的唯一标示符,用来区别其他进程。
• 状态: 任务状态,退出代码,退出信号等。
• 优先级: 相对于其他进程的优先级。
• 程序计数器: 程序中即将被执行的下一条指令的地址。
• 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
• 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
• I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
• 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
• 其他信息
进程 = 内核数据结构(task_struct)+ 程序的代码和数据
运行起来的程序,进程会被根据task_struct属性 被OS调度器调度,运行
为什么要有PCB(task_struct)? OS要管理进程,先描述,再组织。
task_struct 是Linux内核的一种数据结构,它会被操作系统在内存中进行创建,里面包含着进程相关的管理信息。
当可执行程序加载到磁盘中后这个程序(代码)会被加载到内存,同时操作系统为了管理这个进程也会为它创建PCB,这个PCB指向这个代码,然后CPU就开始调度这个进程。
🌟查看进程
• 通过系统目录查看:在 /pro 这个目录下保存着所有的进程信息
• 通过ps命令查看:
ps aux # 查看系统中所有的进程信息
ps axj # 查看进程的父进程号
🌻查看对应进程的信息:
ps axj | head -1 && ps axj | grep myexe
1、把程序运行起来,双击/ ./XX.exe --> 本质就是在系统中启动了一个进程!
进程:
执行完就退出---- ls pwd 等指令;
一直不退,直到用户退出---常驻进程。
在一个系统中随时都可能存在多种进程,每个进程有自己的进程名,进程名是字符串,比较起来非常的麻烦,在计算机内部我们需要给每个进程取一个编号,这个编号就叫做进程的 pid (进程标识符):
同一个可执行程序,在不同的时间点启动的时候,它的pid是会出现变化的,因为在系统层面上,维护进程的pid其实是一个累加的计数器,来记录我们的pid增长的,在启动进程的同时操作系统也不断的启动任务,所以pid会出现变化,而且不连续,如果是连续创建的话,pid就会是连续的。pid是为了区分进程的唯一性。
一个进程在被终止的时候,此时,该进程的所有的属性都没有了:
我们把这个进程关掉后,再执行,在进行查找,进程号一直在变,所以这个进程以及这个属性在 proc 文件里面是实时更新的!
这个可执行程序一直再跑,如果不关掉它,而是直接删除它的可执行程序会出现什么情况呢?
删掉之后,这个进程(可执行程序)还一直在跑,因为它的可执行程序已经被加载到内存了,我们删掉的是磁盘的可执行程序,但是如果我们把这个进程退出了,这个进程就再也启动不起来了(被删掉了)。
当我们在进程的目录里再查找一次它,此时这个进程就意识到这个可执行程序被删除了:
这个 exe 指向的是形成这个进程的原可执行程序文件(在磁盘文件中)。
2、CWD : current work dir 当前工作目录
进程的CWD!
当一个进程再实际中启动的时候,这个进程会记录下自己这个进程启动时,默认所处的路径,然后紧接着用自己的 cwd属性 来把一个进程当前在那个目录下启动的,把这个路径记录下来,当我们记录下来之后,我们的代码就可以跑了。
为什么当我们做文件操作时,不指定文件路径,就会新建到当前路径下?
因为每个进程都会记录下来:
1、自己对应的程序是谁(exe)
2、会记录自己这个程序启动时,所处的工作目录,这个工作目录可以更改。
3、/proc 不是磁盘级别的文件。(不在磁盘保存信息)内存级数据。
换句话说,/proc 这里面的文件不全记录有磁盘里的文件。
🌟通过系统调用获取进程标示符
• 进程id(PID)
• 父进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
命令行中,执行命令/执行程序 , 本质是 -bash 的进程(命令行解释器--外壳程序的简称),创建的子进程,由子进程执行我们的代码。
命令行解释器:
1、命令行解释器-bash,帮我们进行命令行解释;
2、它还不想因为命令行解释出现问题,从而影响到自己,所以_bash往往需要创建子进程,来执行。
-bash: " - " 表示: 代表我们当前用户是使用命令行终端进行登录的
我们该如何 通过 -bash进程,来创建子进程?接着往下看:通过系统调用接口创建进程
🌟通过系统调用创建进程-fork初识
• 运行 man fork 认识 fork :创建一个子进程
• fork有两个返回值
fork一旦创建成功,就会返回子进程的pid给父进程,返回0给子进程;
创建失败,返回-1给父进程,没有子进程会被创建。
• 父子进程代码共享,数据各自开辟空间,但是数据是各自私有一份(采用写时拷贝)
• fork 之后就会变成两个进程,这两个进程是父子关系,通常要用 if 进行分流
fork() 两个进程,代码是共享的,
当可执行程序加载到磁盘中后这个程序(代码)会被加载到内存,同时操作系统为了管理这个进程也会为它创建PCB(父进程),这个PCB指向这个代码,然后CPU就开始调度这个进程。当CPU执行这个代码时,看到fork()这个代码的时候,我们一定创建一个新的进程(fork的本质时系统多了一个进程,进程=内核数据结构+代码和数据),接着操作系统也要给新进程创建PCB(子进程),子进程和父进程相比的区别是父进程有自己PCB之后,它的代码和数据是从磁盘中加载进来的,可是新创建的子进程的PCB并没有从磁盘里再有新的程序和代码,让子进程使用,为此,系统在设定上就 要求 子进程在创建时就指向父进程的代码,即子进程继承父进程的代码(共享)。子进程并没有进行第二次的加载。
但是数据是各自私有的一份。
进程具有很强的独立性!
多个进程之间,运行时,互不影响,即便是父子。
代码--只读;
数据--各自私有一份;
• pid_t id = fork();
1、id 是一个变量;
2、返回的本质,就是向指定变量进行写入返回值数据;
3、打印的本质,就是读取。
创建多进程:
1 #include <iostream>
2 #include <unistd.h>
3 #include <vector>
4 #include <sys/types.h>
5
6 using namespace std;
7
8 const int num = 10;
9
10 void SubProcessRun()
11 {
12 while(true)
13 {
14 cout << "I am sub process, pid: " << getpid() << ", ppid: " << getppid() << std::endl;
15 sleep(5);
16 }
17 }
18
19 int main()
20 {
21 vector<pid_t> allchild;
22 for(int i = 0; i<num ; i++)
23 {
24 pid_t id = fork();
25 if(id == 0)
26 {
27 // 子进程
28 SubProcessRun();
29 }
30 // 这里是谁执行?父进程一个人
31 allchild.push_back(id);
32 }
33 // 父进程
34 cout << "我的所有孩子是:" ;
35 for(auto child: allchild)
36 {
37 cout << child << " ";
38 }
39 cout << endl;
40
41 sleep(10);
42 while(true)
43 {
44 cout << "我是父进程,pid:" << getpid() << endl;
45 sleep(1);
46 }
47
48 return 0;
49 }
50
🌠疑问?
一个函数,fork() ,怎么会有两个返回值?fork函数调用一次,返回两次。
因为fork函数会返回两个返回值,一个是子进程会返回 0 , 一个是父进程会返回子进程的pid。所以会同时进程在两个分支语句中。
当我们在 fork() 时,本质上就是写入,即类似修改全局变量,fork()把自己的核心工作做完之后,此时父子进程都已经运行了,当 return 时,(代码时共享的)父子分别返回各自的值
进程需要独立性!
fork()之后,谁先进行?由OS的调度器自主决定。
🌟进程状态
🌠小贴士:广义的进程状态
1、并行和并发
a、并发:CPU执行进程代码,不是把进程代码执行完毕,才开始执行下一个,而是给每一个进程预分配一个时间片,基于时间片,进行调度轮转(单CPU下);
b、并行:多个进程在多个CPU下分别,同时进行运行。(在一个时间段内,多个进程代码在“同时”推进。)
2、时间片
Linux/windows民用级别的操作系统,分时操作系统(调度任务追求公平)。
3、进程具有独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰;
4、运行状态
只要进程在运行队列中,该进程就叫做运行状态。(即进程已经准备完毕,可以随时被CPU随时调度【在排队】)
5、运行和阻塞的本质:是让不同的进程,处在不同的队列中!
6、等待的本质:连入目标外部设备,CPU不调度!
7、挂起:当内存资源严重不足时,所有被等待的外设进程(阻塞),都有可能会被换出到磁盘的swap分区中(用时间换空间,唤入的时候,很耗费时间),即被挂起到外设上,从而有效的节省内存。这个功能一般会被禁止掉,会导致机器变慢。
8、竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级。
Linux下的进程状态:
我们直接查看Linux内核源代码:
/*
* 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) 磁盘休眠状态 | 有时候也叫不可中断睡眠状态(深度睡眠),在这个状态的进程通常会等待IO的结束,不能被OS杀掉(阻塞等待状态的一种) |
T (stopped) 停止状态 | 可以通过发送sigstop信号给进程来停止进程。这个被暂停的进程可以通过发送sigcont信号让进程继续运行。 (进程做了非法但是不致命的操作,被OS暂停) |
t (tracing stop) | 当进程被追踪(gdb)的时候,断点停下,进程状态就是t |
Z (zombie)僵尸状态 | (检测状态)维持退出信息,方便父进程和操作系统来进行查询 |
X (dead) 死亡状态 | (终止)这个状态只是一个返回状态,你不会在任务列表里看到这个状态 |
🌠小贴士:
前台进程:可直接 (CTRL+C)杀掉的进程;
后台进程: 不能直接终止,若要终止只能: kill -9 17578
🌟僵尸进程
🌠小贴士:
<1> 进程退出:
进程 = 内核数据结构(task_struct)+ 代码和数据
1、代码不会执行了,首先可以立即释放的就是进程对应的程序信息数据(代码和数据);
2、进程退出,要有退出信息(进程的退出码)保存在自己的 task_struct 内部;
task_struct结构体内部有很多的成员属性,成员属性包含有退出信息。
3、管理结构 task_struct 必须被OS维护起来,方便用户未来进行获取进程退出的信息;
<2> 进程的创建与释放
• 创建:先在内核里创建内核数据结构(管理信息),再加载代码和数据。内核的数据结构先创建,只不过在没有加载代码和数据的时候,没有被OS调度。
• 释放:先释放代码和数据,把内核数据结构task_struct维护起来,在代码和数据被释放,内核数据在维护的这个状态就称为僵尸状态
☄️僵尸进程的概念:
• 僵尸状态:当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死进程;
• 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态码;
• 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
在语言层面的内存泄露问题上,如果我们一直申请空间并且没有释放掉,当我们关掉这个进程会造成内存的写了吗?
显然不会,因为当进程退出了之后,首先立即释放的就是进程对应的程序信息数据(代码和数据),但是如果是常驻内存的进程,影响就会比较大。
🌟僵尸进程的危害
☄️僵尸进程危害:
• 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。若父进程一直不读取,那子进程就会一直处于Z状态。
• 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
• 若一个父进程创建了子进程,且一直不回收,就会造成内存资源的浪费!因为数据结构。对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
🌟孤儿进程
父进程先退出,子进程就称之为“孤儿进程”,会被系统领养。
孤儿进程被1号init进程领养,当然要有init进程回收喽。
🌟进程优先级
最终优先级 = pri(默认/老的)+ nice
nice : 优先级的修正数据
进程的优先级:竞争的是CPU资源。
属性 | 含义 |
PRI | 代表这个进程可被执行的优先级,其值越小越早被执行 |
NI | 代表这个进程的nice值( [-20,19] -> 40个级别) |
UID | 代表执行者的身份(用户ID --> 记录了进程是谁启动的!) 1、文件会记录下拥有者,所属组和对应的权限; 2、所有操作,都是进程操作,进程自己会记录谁启动的我; 进而对文件进行权限控制! |
PID | 代表这个进程的代号 |
PPID | 代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号 |
🌟调整进程优先级
调整进程优先级只能调整nice值。
<1> 查看进程优先级的命令:
• top
• 进入top后按 “r” -> 输入进程PID -> 输入nice值
<2> 解释nice:
• nice的取值范围 [ -20, 19 ] 共40个级别。
• 进程优先级的重置:pri (最终) = pri (default 80) + nice ;
• nice要在可控范围内,因为分时OS在进程调度时,是尽量公平的去调度;
• 真实进程的优先级 [60,99]
🌟进程切换
🌠小贴士:
<1>
a、每个进程都有一个时间片,时间片到了,进程就要被切换;
b、Linux 是基于时间片,进行调度轮转的;
c、一个进程在时间片到了的时候,并不一定跑完了,可以在任何地方被重新调度切换;
进程调度前要保留上下文数据,时间片到了就要恢复上下文数据。例如:在校大学生去当兵时,要先保留学籍,以便期满之后,回学校恢复学籍,继续上学。
<2> 调度切换
a、进程在运行的时候,会有很多的临时数据,都在CPU的寄存器中保存;
b、CPU内部的寄存器数据,是进程执行时的瞬时状态信息数据;
c、CPU内有很多个的寄存器,一套寄存器。寄存器 != 寄存器里面的数据。
切换过程和理解:
我们知道我们编写的代码形成可执行程序以后,就会被加载到磁盘上,内存里的OS先在内核里创建内核数据结构(管理信息),再加载代码和数据(可执行程序)进来,那CPU进行执行的时候,是怎么知道执行那行代码的?CPU里面有很多的寄存器,在这里只提及到了一部分:PC、IR、eax、ebx ... ,PC寄存器存储的是 当前正在执行指令的下一条指令的地址;IR指令寄存器,就是正在执行的指令;eax 常被用作主要的操作数存储位置;ebx保存数据或者指针;
当进程不做保护时:进程1突然被停止调度,进程1的数据还没有读取完,CPU就调度进程2的数据,当进程2的数据被读取完或者没读取完,CPU又返回读取进程1的数据时,进程1之前的数据已经被进程2给覆盖住了,难道进程1要被重新读取?显然是不行的。
🎉TSS保存进程的上下文数据。
🎉进程切换的核心:进程上下文数据的保护和恢复。
• 切走:将相关寄存器的内容,保存起来;
• 切回:将历史保存的寄存器的数据,恢复到寄存器中
• CPU寄存器只有一套,是被多个进程共享使用的!
🌟Linux2.6内核的调度算法
以最短的时间选择出一个合适的进程,基于时间片进行轮转、公平调度、考虑优先级。
构建一个指针数值,存有140个链表的头部,不考虑 0-99 这个是实时进程,100-139 40个位置中每一个相当于可以使用优先级数字来进行 hash , hash 能快速根据优先级来确定它应该找哪个队列,而一旦确定出找哪个队列它只需要把队列当中的第一个节点采用头部移出的做法,就找到了指定进程,所以它就可以做到按照优先级的方式,来从左向右进行遍历,来选择进程。
🌠小贴士:
nice 值的范围 [ -20 , 19] , 调整完之后得优先级是 [60,99],根本原因是因为Linux当中的优先级只有40位,因为它得哈希表是设计 40 个优先级的,设计的太多的话,会影响调度的公平性。
在一个CPU中每一个CPU都要有一个自己的运行队列(runqueue),一个运行队列中有两张 hash表,都叫做 queue[140]。
CPU只会从runqueue中的active所指向的指针队列当中进行对应的调度,调度相当于我们切换、运行、时间片到了,选择一个进程。选择进程时,先找到 runqueue --> active 指针,接着用active指针直接进行访问根据特定优先级划分的hash表,然后从里面从上到下进行扫描,队列如果为空就进行下一个,不为空直接从它的散列的头部当中直接选择一个进程,即纵向根据优先级在排队,横向根据到来的先后顺序在排队(横向的优先级都一样)。所以它能很快的去选择一个进程。
🌠小贴士:
• 调度三种情况:
(1)运行退出:退出后,进程不放回调度队列,Z状态/X状态,即退出之后,脱离调度范畴。
(2)不退出,但是时间片到了:不放回active队列,放到expired队列中进行排队。
(3)有新的进程产生了:直接插入到expired队列。
保证了active队列永远都是一个存量进程竞争的情况,即:active队列中进程只会越来越少(时间片会到,进程会退出),不会增多。
• 进程饥饿问题:前面的优先级一直很高一直被调度,使得后面优先级低的进程一直不能被调度。
• 调度器要非常均衡的进行进程调度,优先级很高时,要一直优先吗?不是,新建的进程无论优先级高低都要在 expired队列排队,时间片到了也放到expired队列中进行排队。
• 当active的存量进程被消耗完了,重新规划时间片被链入expired队列当中,操作系统只需交换 active和expired两个指针里面的内容,接着采用同样的调度算法。
• swap:
之前:按照优先级调度;
之后:给了其他优先级的进程的调度机会。
进程叫做被运行起来的程序,进程会被调度,被切换。
🌠小贴士:
1、所有的进程都要链表连接;
2、进程可以在调度队列中,阻塞队列中,这是为什么呢?
Linux中链式结构:双链表结构
• 这样做的意义:一个进程,既可以在全局链表中,又可以在任何一个其他数据结构中,只要加节点字段即可!!!
struct task_struct { // 进程相关属性 struct node listnode; struct node queuenode; struct node waitnode; } // 一个进程,既可以在全局链表中,又可以在任何一个其他数据结构中,只要加节点字段即可!!!
• 原理:
Linux的这种链式结构,可以 根据link成员,就可以访问这个结构体的任意成员。
如若对你有帮助,记得关注、收藏、点赞哦!您的支持是我最大的动力🌹🌹🌹🌹!!!
若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢^ ^ ~