#一、 进程概念
1、1 冯诺依曼体系
1、1、1 中央处理器:CPU
- 功能:算术运算与逻辑运算(与、或、非门)
1、1、2 储存器:内存
- 功能:储存数据,但是不能持久化的保存
- 结论:
- 1、所有数据都是采用二进制进行储存(通过电流的高低电平模拟二进制数据);
- 2、运算产生的数据都是存储在内存中。
- 扩展:
- 1、一个CPU在同一时刻只能计算一组数据、芯片的性能取决于制作工艺;
- 2、4核8C:代表4个物理CPU,8个逻辑CPU;
- 3、交替运算(多任务处理):CPU在同一时间内执行多个程序或任务的能力。在操作系统中,CPU会通过轮流执行不同的程序,每个程序执行一小段时间后,就会被暂停,CPU开始执行另一个程序,直到所有程序都执行完毕。
1、2 操作系统的概念与定位
1、2、1 什么是操作系统
- 简单来说: 操作系统:操作系统内核+一堆程序
- 操作系统概念:
- 操作系统是计算机系统中的核心软件之一,它是管理计算机硬件和软件资源,提供用户和应用程序访问计算机系统资源的程序集合。
- 任何计算机系统都包含一个基本的程序(一堆代码)集合,成为操作系统(OS);
- 操作系统内核:代码程序,代码的作用就是(进程管理、内存管理、文件管理、驱动管理等);
- 一堆应用:依附在操作系统内核上完成某些功能的软件,例如、QQ、微信
1、2、2 操作系统在做什么事
- 操作系统在管理计算机的软硬件资源;
- 硬件资源:CPU、内存、硬盘、网卡、显示器;
- 软件资源:进程资源、驱动程序
1、2、3 操作系统怎样完成这些事情的
- 通过管理。
- 管理 = 描述 + 组织
- 代码角度:
- 描述:结构体;
- 组织:双向链表;
1、2、4 操作系统组织结构图
1、2、5 系统调用&库函数
- 系统调用:操作系统提供的函数,被称为系统调用函数。
- 库函数:C标准库提供的函数、被称之为库函数。库函数的代码实现当中调用了系统调用函数。
1、3进程概念(PCB)
1、3、1什么是程序?什么是进程?
- 程序:源代码经过编译产生的可执行文件,这个文件是静态的。例如、(.out)
- 进程:程序运行起来的实例,是动态在运行的。
1、3、2操作系统是如何管理进程的
- 进程的管理 = 描述(PCB)+组织的方式(双向链表)
1、3、3描述(PCB){task_struct{…}}
- 进程标识符:在当前机器能够唯一表示一个进程
- 进程号(PID:process ID):是操作系统为每个正在运行的进程或线程分配的唯一标识符,一般来说,一个进程由OS自动分配空的进程号。
- 如何查看进程号:
-
命令:
ps aux 加上管道符号和过滤,拿到自己想要的信息;
或者ps -ef -
函数:
pid_t getpid(void):谁调用获取谁的进程号;
pid_t getppid(void):谁调用获取谁的父进号;
-
ps aux |grep <name>
ps -ef |grep <name>
进程状态
- 进程状态:就绪、运行、阻塞;
- 从CPU角度理解:
机器当中的进程很多,CPU的数量远远小于进程的数量,那么进程是如何获取CPU资源的?
重点:一个进程想要向后执行自己的代码,必须拥有CPU资源进行计算(执行代码)
- 操作系统调度进程获取CPU资源:
- 调度策略:
- 先来先服务:先到先服务。在服务需要排队等候的情况下,服务员或服务机构按照顾客的到达先后顺序,依次提供服务。
- 短作业优先:该算法按作业执行所需的时间来排序,先执行最短的作业,以此类推。
- 长作业优先:当有多个作业需要完成时,长作业应该优先完成。
- 优先级优先:优先级高的操作会先被处理,而优先级低的操作则可能被推迟甚至被忽略。
- 时间片轮转(轮询调度):将所有正在运行的进程分配相同的时间片(毫秒级别:5-800),一旦时间片用尽,操作系统就会将该进程暂停并将CPU分配给下一个进程,直到所有进程都被处理一遍。
- 调度策略:
- 运行状态:进程获得CPU资源,占用CPU,并在CPU上运行(进程正在使用CPU来执行自己的代码);
- 就绪状态:进程已经具备运行条件,但CPU还没有分配过来(进程已经将运行前的准备工作全部准备好了,就等着操作系统调用占用CPU);
- 阻塞状态:进程因等待某件事的发生暂时不能运行;例如:等待IO输入、调用某些阻塞接口;
插入:
-
进程是抢占式执行 狼(进程)多肉(CPU)少
在计算机的CPU数量少,进程多的情况下(常态),操作系统在调度的时候要做到雨露均沾。让每一个进程都能运行上,但是操作系统在调度的时候,是从就绪队列当中获取进程,进行运行。(进程准备好了,谁准备好了,原则上就可以调度谁),进程为了能够执行自己的代码,都是抢占式执行,不会互相谦让。
“狼多肉少”========>产生进程的不同状态的主要原因之一; -
并发:多个进程在一个CPU下,采用进程切换的方式,各自独占CPU运行自己的代码(交替运行),让多个进程都得以推进,称之为并发。
-
并行:多个进程加粗样式在多个CPU下,同时运行各自的代码,称为并行。
在一个多核CPU的机器中:并发和并行是混合存在的。 -
细分的进程状态:(重点)
- R:运行状态:
处于R状态的进程,有可能在执行代码,有可能在运行队列(就绪队列); - S:可中断睡眠状态:
进程正在睡眠(被阻塞),等待资源到来时唤醒,也可以通过其他进程信号或时钟中断唤醒进入运行状态; - D:不可中断睡眠状态:
通常等待一个IO结束(也就是输入输出的结束); - T:暂停状态(Ctrl+Z):
在Linux下,Ctrl+z不是结束进程,而是暂停。终止进程:Ctrl+c; - t:跟踪状态:
调试程序的时候可以看到; - X:死亡状态:
这个状态是用户看不到的,在PCB被内核释放的时候,进程会被置为X,紧接着进程就推出了; - Z:僵尸状态(重点):(后面会详细说明)
当一个进程已经完成了自己的任务,但其父进程却还没有执行完“回收”该进程的操作,因此该进程被称为僵尸进程。
- R:运行状态:
-
程序计数器:
保存程序下一条执行的指令(重点)- CPU 进行运算的时候,是按照汇编指令进行运算的。
-
上下文信息:
保存寄存器当中的内容(重点) -
在多进程的操作系统当中,进程切换(操作系统调度进程,在获取CPU之后),进行恢复现场(通过程序计数器和上下文信息),继续执行后面的代码。
-
CPU与内存的抽象关系图:
-
PCB结构体:
struct task_struct{
//进程标识符
//进程状态
//程序计数器
//上下文指针
//内存指针
//记账信息
//IO信息
//这些只是冰山一角
}
-
内存指针:指向”程序地址空间“(重点);
-
记账信息:记录使用CPU时长,内存大小;
-
IO信息:保存进程打开文件的信息;
- /pro/[pid]:进程存在于磁盘的文件信息,一个进程是一个以pid命名的文件夹;
例如: - 一个进程被创建出来,默认可以使用标准输入、标准输入、标准错误(stdin、stdout、stderr),原因就是在进程创建之初,默认打开了这三个文件(Linux当中一切皆文件)
- /pro/[pid]:进程存在于磁盘的文件信息,一个进程是一个以pid命名的文件夹;
-
扩展话题:
- 结论一: 以32位操作系统为例,程序地址空间当中的4G空间,每一个字节都有一个地址编号(0x00000000~0xFFFFFFFF);
- 大端和小端:
- 小端:低位存在低地址;
- 大端:低位存在高地址;
- x86体系的机器都是小端机器;
1、3、4组织(双向链表)
1、3、5创建子进程
1、fork()
- 头文件<unistd.h>
#include<unistd.h>
pid_t fork(void)
- 作用:让一个正在运行的进程调用该函数,可以让运行的进程创建出来一个子进程。两者是父子关系;
- 参数:无参;
2、fork()的返回值
RETURN VALUE:
Upon successful completion, fork() shall return 0 to the child process and shall return the process ID of the child process to the parent process. Both processes shall continue to execute from the fork() function. Otherwise, -1 shall be returned to the parent process, no child process shall be created, and errno shall be set to indicate the error.
- 失败:返回-1;
- 成功:返回两次,
父进程返回大于0的数字;
子进程返回等于0
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 printf("begin create sub process....\n");
7 /*创建子进程*/
8 fork();
9 printf("end....\n");
10 return 0;
11 }
3、原理
- 子进程拷贝父进程的PCB:
- 结论:
1、父子进程代码共享,因为子进程拷贝父进程的PCB,所以拥有相同的代码;
2、数据独有(栈的数据,堆区的数据):因为各自有各自的程序地址空间;
3、父进程将子进程创建出来后,子进程就是一个独立的进程,被操作系统独立进行调度(调度的时候和父进程没有半毛钱关系),并且父子进程还是抢占式执行;引申含义:父进程创建子进程成功之后,父子进程谁先被调度谁被后调度,还是不一定的;
- 结论:
- 引出问题:子进程被创建出来之后,到底是从main函数开始执行呢,还是从fork之后开始运行呢,为什么?
4、子进程从哪一行开始执行?
-
从头开始执行(×,子子孙孙无穷无尽也)
-
从fork对应的代码行开始执行(×,子子孙孙无穷无尽也)
-
从fork 之后开始运行;
-
原理解释:
程序计数器:保存程序要执行的下一条指令;
5、使用fork()使两个进程执行不同的代码
1: test_fork.c ⮀ ⮂⮂ buffers
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main(){
5 pid_t i= fork();
6 if(i<0){
7 printf("create error");
8 }
9 else if (i==0){
10 printf("I am child process ,pid=%d,ppid=%d\n",getpid(),getppid());
11 }
12 else{
13 printf("I am father process,pid=%d,ppid=%d\n",getpid(),getppid());
14 }
15 while(1){
16 sleep(1);
17 }
18 return 0;
19 }
6、扩展: 正常在命令行当中启动的进程,他的父进程是谁呢?
shell命令以及运行原理:
-
Linux严格意义上说是一个操作系统,我们称之为“核心(kernel)”,但我们一般用户,不能直接使用kernel。而是通过kernel的“外壳”程序,也就是所谓的shell ,来与kernel沟通。如何理解?
-
从技术角度:
Shell的最简单定义:命令行解释器(command Interperter)主要包含:
1、将使用者的命令翻译给核心(kernel)处理;
2、同时,将核心的处理结果翻译给使用者。 -
对比于windows GUI:我们操作windows不是直接操作Windows内核,而是通过图形接口,点击,从而完成我们的操作。
-
shell对于Linux:有相同的作用,主要是对于我们的指令进行解析,指令解析给Linux内核,反馈结果在通过内核运行出结果,通过shell解析给用户。
-
命令行解释器本质也是一个进程!!!!!
-
在命令行当中启动一个进程,原理是:
- 命令行解释器的进程,创建子进程,让子进程进行程序替换(进程控制),替换为目标程序。
1、3、6 僵尸进程&僵尸状态
1、什么是僵尸进程
- 僵尸进程:已经完成执行的子进程,但是其父进程尚未对其进行善后处理,导致进程表中仍存在该进程的记录,但是已经不再具有任何功能和作用的进程。
2、模拟代码
kill <pid> // 终止一个正在运行的进程
kill -9 <pid> //强杀一个进程
//与进程信号强相关
4 int main()
5 {
6 pid_t ret=fork();
7 if(ret<0)
8 {
9 printf("crete failed....\n");
10 return 0;
11 }
12 else if(0==ret)
13 {
14 //子进程
15 //sleep(1);
16 printf("i am child process pid=%d,ppid=%d\n",getpid(),getppid( ));
17 }
18 else
19 {
20 printf("i am father process pid=%d,ppid=%d\n",getpid(),getppid ());
21 while(1)
22 {
23 sleep(1);
24 }
25 }
26 return 0;
27 }
- 使用kill命令
- 不能让一个已经“死”了的进程再“死”一次
3、原因
- 子进程先于父进程退出,子进程在退出的时候,会告知父进程(信号),父进程接收到信号之后是忽略处理,父进程并没有回收子进程的退出状态信息,从而导致子进程变成了僵尸进程。
- 退出状态信息:
退出码、退出信号、core dump标志位;
4、僵尸进程的危害
- 子进程的PCB(struct task_struct{…})没有被操作系统内核释放,从而导致内存的泄露。
5、如何解决僵尸进程
- 1、终止其父进程(孤儿进程,不推荐,成本太高)
kill <父进程PID>`
kill -9 <父进程pid>
- 2、重启操作系统(简单,粗暴,不推荐,成本太高)
- 3、进程等待(进程控制)
1、3、7 孤儿进程
1、产生原因
- 父进程先于子进程退出,子进程就变成了孤儿进程;
2、代码模拟产生孤儿进程
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main(){
5 pid_t res=fork();
6 if(res<0){
7 printf("create error!");
8 return 0;
9 }else if (res==0){
10 while(1){
11 printf("I am child process,pid=%d,ppid=%d\n",getpid(),getppid());
12 sleep(3);
13 }
14 }else{
15 printf("I am father process.\n");
16 while(1);
17 }
18 return 0;
19 }
3、孤儿进程的退出信息由谁回收ne?
- 由一号进程(养父)回收:该孤儿进程在退出的时候,退出状态信息由1号进程进行回收;
- 一号进程:操作系统的init进程,操作系统很多进程都由该进程创建出来的。
插入:
- 在命令行当中运行的进程,一般情况下都是阻塞bash进程的运行的。
- 前台进程:阻塞bash运行的进程称之为前台进程,反之,称之为后台进程
- 前台程序运行时+&=====>后台进程
- 后台进程+“fg”=======>前台进程
4、 孤儿进程有危害ma?
- 孤儿进程没有任何危害,1号进程回收孤儿进程的退出状态信息;
- ** 有孤儿进程,没有孤儿状态!!!!**
1、3、8 环境变量
1、什么是环境变量
- 环境变量:在操作系统中用来指定操作系统运行的一些参数;换句话说,操作系统通过环境变量来找到运行时的一些资源。
- 例如,链接的时候,帮助连接器找到动态库(标准库的);(用户自己编译的动态库,需要自己指定环境变量),执行命令的时候,帮助用户找到该命令在哪一个位置,例如我们常用的ls。
2、常见的一些环境变量
- PATH:指定可执行程序的搜索路径,程序员执行的命令之所以能找到,这个环境变量起到的作用(汗马功劳)
- which <命令>:返回该命令的可执行文件路径
- HOME:登录到Linux 操作系统的用户家目录
- shell:当前命令行解释器,默认是“/bin/bash”
3、查看当前的环境变量
- env
环境变量名称:环境变量的值(使用“:”进行间隔)
- echo $<环境变量名称>
可以查看单个环境变量对应的值
4、环境变量对应的文件
- 系统级文件:针对于各个用户都起作用(root用户修改),强烈不推荐修改系统级的环境变量文件,因为会影响其他用户
/etc/bashrc - 用户级别环境变量文件:推荐大家修改这两个文件,只对自己用户的环境变量做出修改,只影响自己,这两个文件在当前用户的家目录下面,是隐藏文件。
~/.bashrc
~/.bash_profile
~/.bash_profile文件包含/.bashrc文件;
~/.bashrc包含/etc/bashrc文件; - eg:
在用户hty_dsb下三个文件的关系:- ~/.bashrc
- ~/.bash_profile
- /etc/bashrc
- ~/.bash_profile
- ~/.bashrc
5、修改环境变量
-
命令范式:export <环境变量名称>=<$环境变量名称>:<新添加的环境变量的内容>
-
export PATH=/home/hty_dsb
- 结果:PATH=/home/hty_dsb
- 结果:PATH=/home/hty_dsb
-
export PATH=$PATH:/home/hty_dsb
- 结果:在原先环境变量的内容保持不变的情况下,增添新内容/home/hty_dsb
- 结果:在原先环境变量的内容保持不变的情况下,增添新内容/home/hty_dsb
-
命令行中直接修改:(特点:临时生效)
- 如果是新增,可以不要<$环境变量名称>,及export <环境变量名称>=<新添加的环境变量的内容>;
- 如果是修改老的:必须加上<$环境变量名称>,否则之前的环境变量的值就找不到了。
-
文件中修改:
- 特点:修改完毕之后,不会立即生效,需要配合source <环境变量文件名称>,永久生效;
- 新增:在文件末尾直接添加:export <环境变量名称>=<新添加的环境变量的内容>:<新添加的环境变量的内容>,用:分隔每个内容;
- 如果修改老的:在老的后面添加“:<新添加的环境变量的内容>”。
-
扩展内容:
- 让自己的可执行程序在命令行当中运行的时候,不需要加上“./”;
- 操作方法:将可执行程序的路径配置到PATH环境变量当中。
- “./”的作用:告诉bash,要执行程序在当前路径下。
6、环境变量的组织方式
- 环境变量是以字符指针数组的方式进行组织的,最后的元素以NULL结尾(当程序拿到环境变量的时候,读取到NULL,就知道读取完毕了)
7、代码获取环境变量
1、main函数的参数
- int main(int argc,char* argv[],char* env[])
1 #include<stdio.h>
2 #include<stdlib.h>
W> 3 int main(int argc,char* argv[],char* envp[])
4 {
5 for(int i=0;envp[i]!=NULL;i++)
6 {
7 printf("---------[%s]\n",envp[i]);
8 }
9 return 0;
10 }
- 扩展知识:命令行参数
- eg:
ls -a -l : -a -l ,就是命令行参数;
- eg:
1 #include<stdio.h>
2 #include<string.h>
3
4 int main(int argc,char* argv[])
5 {
6 for(int i=0;i<argc;i++)
7 {
8 if(strcmp("-a",argv[i])==0)
9 {
10 printf("-a read.....\n");
11 }
12 }
13 return 0;
14 }
2、environ
- extern char **environ:这个是全局的外部变量,在lib.so(c标准库当中定义的)当中定义,使用的时候,需要用extren关键字
1 #include<stdio.h>
2 #include<string.h>
3 #include<stdlib.h>
4
5 int main()
6 {
7 extern char** environ;
8 for(int i=0;environ[i]!=NULL;++i)
9 {
10 printf("-------[%s]\n",environ[i]);
11 }
12 return 0;
13 }
3、getenv
- #include<stdlib.h>
- char *getenv (const char *name);
- 参数:环境变量名称;
- 返回值:环境变量的值,没有找到返回NULL;
-### 1、3、9 程序地址空间
- char *getenv (const char *name);
5 int main()
6 {
7 printf("----HOME%s\n",getenv("HOME"));
8 return 0;
9 }
1、3、9进程虚拟地址空间
1、图示
2、代码
-
步骤:
- 1、定义一个全局变量
- 2、创建一个子进程
- 3、父子进程分别打印全局变量的值,打印全局变量的地址
- 4、子进程修改全局变量,继续观察
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 地址值是相同的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做虚拟地址
问题:为啥操作系统费劲的要搞出来一个虚拟地址呢?
- 解决内存碎片问题 ,提高内存使用率
3、虚拟地址
- 我们在用C/C++语言所看到的地址,都是虚拟地址!物理地址用户一概看不到,由操作系统统一管理;
- 操作系统需要负责将程序当中的虚拟地址,转化为物理地址。
4、进程的虚拟地址空间
- 操作系统为每一个进程都虚拟出来一个4G的虚拟地址空间(32位操作系统),程序在访问内存的时候,使用的是虚拟地址进行访问。
- 既然是操作系统虚拟出来的地址,所以并不能直接存储数据,存储数据还是在真实的物理内存当中。
- 所以OS需要将虚拟地址转化为物理地址进行访问(页表)。
扩展
- 为什么操作需要给每个进程都虚拟出来一个进程地址空间呢?为啥不直接让进程访问物理内存,这样不是更加快一点
- 原因:因为各个进程访问同一个物理地址空间,就会造成不可控。在有限的内存空间中,进程是不清楚哪一个内存被其他进程使用,那些内存是空闲的,没有办法知道。所以,在这种场景下,冒昧的使用,一定会导致多个进程在访问物理内存的时候,出现混乱;所以,内存由操作系统统一管理起来,但是又不能采用预先直接分配内存的方式,给进程分配物理内存。因为操作系统也不清楚进程能使用多少内存,使用多久。所以,操作系统就虚拟给每个进程分配了4G的地址(虚拟地址)。
- 当进程真正要保存数据,或者申请内存的时候,操作虚拟地址,让操作系统再给进程进行分配。这样就比较合理(同时也会节省很多空间,毕竟谁用才分配真正的物理内存)。
- 每个进程都无感的使用拿到的虚拟地址,背后就是操作系统进行了转换(就是后面说到的页表映射)。
- 为什么32位操作系统,给进程分配的是4G的虚拟地址空间?
- 因为32位操作系统总共有32根地址线,每个地址能模拟的数字为0/1,所以最小就是0x00000000,最大就是0xFFFFFFFF。 64位同理。
5、页表
-
1、图解
-
2、映射关系:
虚拟地址空间分成一页一页
物理地址分成一块一块
使用页表将页和块映射起来
扩展
-
如何通过虚拟地址+页表的方式找到具体的物理地址
- 虚拟地址 = 页号 + 页内偏移
- 页号 = 虚拟地址 / 页的大小(4096(4kb))
- 页内偏移 = 虚拟地址 % 页的大小
-
为什么内存分配的时候,不采用连续分配的方式,而是一页一页离散分配的方式
- 防止内存碎片,使小空间也可以被利用起来。
- 防止内存碎片,使小空间也可以被利用起来。
-
fork创建子进程的时候,会不会拷贝页表?
- 会,每个进程都有自己的页表,子进程最初的页表映射的内容就是来自于父进程。
-
分段式:
- 虚拟地址 = 段号 + 段内偏移
- 段表 段号 :段的起始地址
-
段页式:
- 虚拟地址 = 段号 + 页号 + 页内偏移
- 段表 —> 段号:页表的起始位置
- 页表 —> 页号:块号
1、3、10 扩展问题
- task_struct结构体代码
之后的学习离不开task_struct结构体,例如文件描述符,进程信号,多线程等等的学习。
1、3、11 进程优先级问题
- PRI:进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小,进程的优先级别越高,反之
- 那NI呢?就是我们所说的nice值了,其表示进程可被执行的优先级的修正数值。
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级别变高,则其越快被执行,
所以,调整进程优先级,在Linux下,就是调整进程的nice值,
nice:取值范围是-20~19,一共40个级别。 - 用top命令更改已存在进程的nice:(root用户进行修改)
top
->进入top后按“r”
->输入进程的PID
->输入nice值