输入:键盘,话筒,摄像头,磁盘,网卡
输出:显示器,音响,磁盘,打印机
存储器:内存
cpu
(运算器+控制器)运算器:运算计算(+,-,*,/)+逻辑计算(|,&,^)
控制器:协调数据流向,流多少
外设不与
cpu
打交道,外设输入输出数据,是写入内存或从内存中读取
cpu
只能对内存读写,不能访问外设比如:使用
从键盘外设输入数据,数据放入到内存,
cpu
读取内存中的数据,进行计算,再放入到内存中,再把这个数据输出到网卡中,通过网络,数据到达好友的网卡(外设输入),数据进入内存,cpu
读取内存中的数据,经过cpu
的计算,再放到内存中,输出到显示器上。
在我们写完一个代码,经过编译成一个可执行程序,要运行这个程序,需要先把这个程序加载到内存,为什么要加载到内存?
体系结构规定
对于硬件来说,只能被动的完成某种功能,不能主动的完成某种功能,只能通过软件(操作系统)来进行
操作系统本质上就是搞管理的软件
操作系统管理硬件,并不是直接管理硬件,而是通过操作系统管理驱动程序,驱动程序管理硬件来间接管理硬件。
进程:是一个运行起来的程序(程序加载到内存)
管理是对信息的管理,操作系统管理进程,就是先将进程数据化,再管理这个数据,但是在操作系统中会同时存在大量进程,所以就需要对这些进程数据化后的数据进行组织。
在
linux
中,是先将进程的代码和数据先加载到内存,再对进程的数据化,是采用结构体
task_struct
(pcb
:进程控制块)(在这个进程控制块内存储进程所有的属性数据),对进程的组织化,就是将多个进程的
pcb
通过双链表来链接起来由此得出:进程 = 可执行程序代码,数据 + 该进程对应的内核数据结构
操作系统不会直接暴露自己的任何数据结构,代码逻辑,其他数据相关的细节
操作系统是通过系统调用的方式,对用户提供接口服务
linux
是由c语言写的,这里所谓的系统调用接口,本质就是c语言接口
每个进程都有一个唯一的标识符
pid
ps axj|grep 'mytest'|grep -v grep
//查找mytest
进程
在/
proc
中存放实时进程信息,/proc
/pid
中存放该pid
号的进程信息
cwd
:进程当前的工作路径工作路径:该进程在那个路径被打开的,那个路径就叫工作路径
exe
:进程对应的可执行程序的磁盘文件可执行程序是文件,存储在磁盘中
进程id(
PID
)父进程id(
PPID
)#include
#include int main() { printf("该进程的pid为:%d\n",getpid()); printf("该进程的父进程的pid为:%d\n",getppid()); return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
杀掉进程
kill -9 pid //杀掉进程
- 1
由下图可知,这两个进程的
pid
的不同,但是它们的父进程的pid
相同
这个父进程为
bash
,几乎我们在命令行上所执行的指令,都是bash进程的子进程
- fork有两个返回值
- 父子进程代码共享,数据各自开辟空间(由写时拷贝实现)
- fork会给父进程返回子进程的
pid
,给子进程返回0
#include
#include
int main()
{
int ret=fork();
if(ret==0)
{
printf("hello world,pid:%d,ppid:%d\n",getpid(),getppid());
}
else
{
printf("hello byld,pid:%d,ppid:%d\n",getpid(),getppid());
}
return 0;
}
fork之后,创建出子进程,父进程和子进程会共享全部代码,一般会执行后续代码
fork之后,创建出子进程,这个子进程是如何创建的?
fork之后,内存中就会存在子进程的
task_struct(pcb)
+子进程的代码和数据子进程的
task_struct(pcb)
以父进程的task_struct(pcb)
为模板初始化(并不是完全一样)
printf
为什么会打印两次?
因为
fork()
返回值不同,通过返回值不同,判断,让父子执行不同的代码块
fork为什么会有两个返回值?
因为fork函数返回了两次
那么fork函数为什么会返回两次?
因为fork函数是创建子进程的,在到达fork函数中的return语句时,这个函数的核心功能已经实现了,子进程已经创建了,并且放入到运行队列中,这时有一个父进程,一个子进程,两个进程分别执行return语句,返回两个
pid
如何理解进程被运行?
在CPU中,有一个运行队列
runqueue
,runqueue
中会有一个指针,会指向这个双向链表的第一个节点(pcb
),等待被调度,调度这个进程时,这个pcb
会被保存到CPU的寄存器中,CPU就会通过这个pcb
找到这个进程代码,将代码加载到CPU中的pc
指针,执行。在创建一个子进程之后,这个子进程放入运行队列,等到调度这个进程时,因为这个子进程和父进程共享代码,就会找到父进程的代码,执行。
fork为什么给父进程返回子进程的
pid
,给子进程返回0 ?
父进程必须标识子进程的方案(因为一个父进程可以有多个子进程),fork之后,需要给父进程返回子进程的
pid
子进程最重要是要知道自己被创建成功了,因为子进程找父进程成本低(一个子只有一个父,不用标识)
在操作系统,进程状态大致分为四种
- 运行态
- 终止态
- 阻塞态
- 挂起态
**概念:**进程只要在运行队列中就叫运行态,随时可以调度
**概念:**进程依旧存在,但是永久不运行,随时等待被释放
概念:进程等待某种资源(非CPU),资源没有就绪的时候,进程需要在该资源的等待队列中进行排队,此时进程的代码并没有运行,进程所处的状态叫做阻塞
一个进程,在使用资源的时候,不仅仅是在使用CPU资源,进程可能申请其他资源:磁盘,网卡,显卡,显示器资源,声卡,音响
如果我们申请CPU资源,展示无法满足,需要排队——运行队列
那么如果我们申请其他慢设备的资源呢?
也是需要在这些慢设备的等待队列中排队
因为操作系统管理硬件资源,将硬件资源数据化,所以这些硬件都有自己对应的结构体,在结构体中会有对应硬件的属性信息以及等待队列
当进程访问某些资源(磁盘,网卡),该资源如果暂时没有准备好,或者正在给其他进程提供服务,此时,当前进程(
pcb
)要从**runqueue
(运行队列)中移除,将当前进程放入对应设备的描述结构体的等待队列**当这个进程在等待外部资源时,该进程的代码不会被执行(进程卡住了,进程阻塞)
当外部资源准备就绪后,将进程的**
pcb
放回到CPU的运行队列**中这些工作都是由操作系统完成的,操作系统对进程的管理工作
将源文件编译成可执行程序(
.exe
),是存储在磁盘中的,将可执行程序加载到内存中,将它数据化成pcb
,在内存中就会存在这个进程内核的数据结构 + 对应的代码和数据,如果有多个进程同时运行,内存中就会有多份进程内核的数据结构 + 对应的代码和数据,这时,可能会导致内存的空间不足。为了解决这个问题,就有了进程挂起
进程挂起:短期内不会被调度(等待的外部资源不会被就绪),他的代码和数据依旧在内存上,会导致空间浪费,操作系统就会把该进程的代码的数据置换到磁盘的swap分区
所以,往往内存不足的时候,伴随着磁盘被高频率访问
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 */ };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
在运行队列中
浅度睡眠,可以杀掉,可以随时叫醒(操作系统和用户(可以被杀掉)都可以唤醒它)
在等待资源,资源就绪,操作系统将其变为R状态(运行状态)
#include
#include int main() { while(1) { printf("hello world\n"); sleep(1); } return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
这个进程大部分时间都在等显示器资源,所以处于S(阻塞状态)
深度睡眠,不可杀掉
在Linux中,如果我们等待的是磁盘资源,进程阻塞所处的状态就是D
当Linux中的进程退出的时候,不会直接进入X(死亡状态),而是先进入僵尸状态
为什么要进入僵尸状态?
因为当这个进程执行结束,退出,进程需要把它的任务完成的如何告诉父进程,将进程的执行结果告知父进程
僵尸状态,就是为了维护退出信息(退出信息会写入到
pcb
中),可以让父进程或者操作系统读取的。(如何读取呢? 通过进程等待让父进程读取)
该状态下的进程,可以被释放
如何模拟僵尸状态?
创建子进程,子进程退出,进入僵尸状态,父进程不退出,也不进程等待(接收子进程的结果)子进程
长时间僵尸,有什么问题?
如果僵尸状态的进程不被回收,该进程就会一直僵尸进程,会导致内存泄漏
僵尸进程不能被杀死??
模拟僵尸状态
#include
#include int main() { int id=fork(); if(id==0) { printf("该进程为子进程,pid为%d,ppid为%d\n",getpid() ,getppid()); } else { while(1) { printf("该进程为父进程,pid为%d,ppid为%d\n",getpid(),getppid()); sleep(1); } } return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
孤儿进程
子进程还在运行,但是父进程提前退出
父进程退出,为什么没有进入僵尸状态,而是直接没了
如果父进程提前退出,子进程还在运行,父进程依旧会进入僵尸状态,只不过父进程很快会被bash进程回收,进入死亡状态,释放,而子进程会被1号进程(就是操作系统)领养
模拟孤儿进程
#include
#include int main() { int id=fork(); if(id==0) { while(1) { printf("该进程为子进程,pid为%d,ppid为%d\n",getpid(),getppid()); sleep(1); } } else { printf("该进程为父进程,pid为%d,ppid为%d\n",getpid(),getppid()); } return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
进程被调试的时候,遇到断点的状态
kill - 9 杀死进程
kill -19 暂停进程
kill -18 继续进程
在进程状态中我们可以看到有的进程后有+
状态后面有+是前台进程(可以
ctrl+c
杀掉进程)后台进程,不能用
ctrl+c
,只能用kill -9 杀掉
优先级vs权限
优先级:是进程获取资源的先后顺序
权限:是能还是不能的问题
优先级存在是因为在系统内,进程占大多数,而资源是少数,进程竞争资源是常态,一定要确定优先级
ps -l
//查看与bash
进程相关的进程在进程信息中,会看到
PRI
和NI
,
UID
:代表执行者身份PID
:代表这个进程的代号PPID
:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号PRI
:代表这个进程可被执行的优先级,其值越小越早被执行NI
:代表这个进程的nice值要更改进程优先级,需要更改的不是
PRI
,而是NINI:进程优先级的修正数据
Linux不允许进程无节制的设置优先级
PRI=PRI_OLD+NI
//每次设置优先级,这个PRI_OLD
都会被恢复为80
NI
范围[-20,19]
PRI
范围[60,99]
用top
命令更改已存在进程的nice:
- top
- 进入top后按“r”–>输入进程
PID
–>输入nice值
竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰(进程运行具有独立性,不会因为一个进程挂掉或者异常,而导致其他进程出现问题)(进程通过进程地址空间使其具有独立性)
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
我们的电脑大多都是单CPU,但是我们的电脑有多种进程在跑???
当这个进程运行时,并不是直到这个进程运行结束,这个进程一直占用这个CPU,我们的操作系统大多都是分时的
操作系统会给每一个进程,在一次调度周期中,赋予一个时间片的概念,一个进程的时间片运行完,就运行下一个进程,到下一个调度周期,继续上一次的运行,这样就可以在一个时间段内,多个进程会通过切换交叉的方式,让多个进程代码,在一段时间内得到推进(能够继续推进,是因为CPU中有
pc
指针(程序计数器)),这就叫做并发
操作系统,不是根据简单的队列顺序进行调度的,而是抢占式内核
当一个进程正在运行,如果来了一个优先级更高的进程,调度器会将运行的进程从CPU上剥离下来,放上优先级更高的进程,这就是进程抢占
在CPU中的寄存器,可以临时存储数据
在进程在CPU上运行时,会产生大量数据,这些数据会暂时存放在CPU的寄存器上,在寄存器上的数据,我们称之为这个进程的上下文数据,当这个进程的时间片结束或者被进程抢占,这个进程会从CPU上被剥离,在寄存器上的上下文数据会被存储到这个进程的
pcb
中,等下次调度这个进程时,再从pcb
中拿到上下文数据,放到寄存器中,继续执行。
为什么我们的代码运行要带路径,而系统的指令不用带路径 ?(系统的指令也是程序)
因为系统中存在相关的环境变量,保存了程序的搜索路径
系统中存在一个环境变量:PATH,可以搜索可执行程序
PATH中保存的是可执行程序的路径,当使用指令是,就在PATH中搜索对应的路径,找到指令,执行,停止搜索
echo $ PATH //输出PATH内容,加上$是显示PATH内容,不加$就把PATH当成字符串输出了
- 1
在PATH这个环境变量中存储了可执行程序的路径(用
:
分隔)
在命令行中也可以定义变量
命令行变量分两种:
- 普通变量
- 环境变量
我们在上图中定义的是普通变量,所以
env
中没有,需要将普通变量导出到环境变量export aaa //将aaa导出到环境变量 upset //取消环境变量
- 1
- 2
如何让自己的程序不带路径运行?
把我的
exe
考到usr/bin/
中(不建议)(usr/bin/中存储的是系统的指令(可执行程序))把我的
exe
所处的路径添加到PATH中export PATH = $PATH:路径 //把自己的路径导入PATH
- 1
which 查命令也是在环境变量中搜索的
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash
echo $
HISESIZE
保存命令行数history 查询写过的命令
history |wc -l
统计写过的命令条数(最多保存3000条)set 查看本地变量和shell变量
main函数可以带参数吗 ?
int main(int argc,char* argv[],char* env[]) { return 0; } //int argc argv数组中的元素个数 //char* argv[] argv叫做命令行参数,传入的是程序名和选项 //char* env[] 传入环境变量
- 1
- 2
- 3
- 4
- 5
- 6
- 7
命令行参数的意义是什么 ?
同一个程序,不同的选项,执行不同的执行逻辑,执行结果
Linux系统中,会根据不同的选项,让不同的命令,可以有不同的表现
获取环境变量的三种方式
- main函数的第三个参数
- c语言提供的第三方变量,
extern char** envision
getenv("PATH")
//获取特定环境变量
环境变量具有全局属性 ?
命令行中的进程的父进程都是BASH进程
本地变量:本质就是bash进程内部定义的变量,不会被子进程继承下去
环境变量:具有全局性,会被子进程继承下去
在上面,我们说过,命令行中的进程都是BASH进程的子进程,本地变量在BASH内部,不会继承到子进程,就是在子进程中本地变量无效,但是上图中echo作为一个子进程,本地变量却有效为什么?
Linux下大部分命令都是通过子进程的方式执行,
但是,还有一部分命令,不通过子进程的反射光是执行,而是由BASH进程自己执行(调用自己对应的函数来完成特定的功能),我们把这种命令叫做内建命令(比如:echo)
我们以前了解到的程序地址空间,以操作系统的概念,是进程地址空间
进程地址空间
进程地址空间不是内存 !
#include
#include int a=0; int b; int main(int argc,char* argv[],char* env[]) { printf("code address: %p\n",main); printf("initial address: %p\n",&a); printf("unintial address: %p\n",&b); char*c1 = (char*)malloc(sizeof(char)*6); char*c2 = (char*)malloc(sizeof(char)*6); char*c3 = (char*)malloc(sizeof(char)*6); char*c4 = (char*)malloc(sizeof(char)*6); printf("heap address: %p\n",c1); printf("heap address: %p\n",c2); printf("heap address: %p\n",c3); printf("heap address: %p\n",c4); printf("stack address: %p\n",&c1); printf("stack address: %p\n",&c2); printf("stack address: %p\n",&c3); printf("stack address: %p\n",&c4); printf("agrv address: %p\n",argv[0]); printf("env address: %p\n",env[0]); return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
经过验证,进程地址空间是符合上图的,地址依次增加,堆和栈相向而生,栈向地址减小的方向生长,堆向地址增大的方向生长
#include
#include int main() { int _val=0; pid_t id=fork(); printf("数据修改前\n"); if(id==0) { printf("该进程为子进程,pid:%d,ppid:%d,_val值为%d,_val地址为%p\n",getpid(),getppid(),_val,&_val); } else { printf("该进程为父进程,pid:%d,ppid:%d,_val值为%d,_val地址为%p\n",getpid(),getppid(),_val,&_val); } printf("数据修改后\n"); if(id==0) { _val=2; printf("该进程为子进程,pid:%d,ppid:%d,_val值为%d,_val地址为%p\n",getpid(),getppid(),_val,&_val); } else { printf("该进程为父进程,pid:%d,ppid:%d,_val值为%d,_val地址为%p\n",getpid(),getppid(),_val,&_val); } return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
通过上面的程序,我们可以发现,当父子进程没有人修改全局数据时,父子是共享该数据的,父子进程读取同一个变量(地址一样),但是后序如果修改了这个数据,父子进程访问的地址依旧一样,但是得到内容却不同,这说明,这个地址一定不是物理地址,而是虚拟地址,那进程地址空间并不是物理内存,而是虚拟内存
那么为什么操作系统不让我直接看到物理内存?
因为内存是硬件,不能阻止你访问,为了防止用户对内存上的数据造成破坏。
进程地址空间是什么?
每一个进程都会有一个进程地址空间,在进程启动的时候,都会让操作系统给他创建一个地址空间,该地址空间就是进程的地址空间
因为,操作系统需要管理进程地址空间,那么就需要对进程地址空间进行先描述,在组织
那么,如何对进程地址空间先描述,在组织 ?
进程地址空间就是内核的一个数据结构,
struct mm_struct
,进程的pcb
(task_struct
)也是内核的数据结构,在pcb
中有一个指针,指向这个mm_struct
(进程地址空间),进程地址空间是虚拟地址,虚拟地址通过页表映射到物理地址,通过物理地址就找到了真正存储在物理内存的位置为了保证进程的独立性,保证多进程运行期间互不影响
进程的内核数据结构是独立的,进程的代码和数据是独立的
**总结:**磁盘中的程序和数据加载到物理内存中,对进程进行描述化,组织化,构建
task_srtruct
和mm_struct
,操作系统会给每一个进程构建页表结构,页表中虚拟地址和物理地址建立映射关系
什么叫做区域?
在
mm_struct
中存在多个结构体,进程地址空间中分为多个区域(栈区,堆区,代码区…………),每一块区域就是一个结构体(struct
),多个结构体通过链表连接起来**补充:**程序编译完之后,没有加载到内存,由地址,由区域都在磁盘中划分好了
数据结构独立:
创建子进程时,创建子进程的内核数据结构(
mm_struct
,task_struct
),对子进程的数据结构进行初始化(父进程为模板),创建子进程,OS也会给子进程创建子进程的页表(子进程的页表也是以父进程为模板初始化)
代码和数据独立:
数据独立:
在刚创建出子进程时,子进程的数据结构,页表都是以父进程为模板初始化,所以没有改变数据时,父进程和子进程的数据对应的虚拟地址相同,页表的映射关系相同,父子进程同一个数据映射到同一个物理地址
但是,如果对数据进行改变,就会放生写时拷贝,将父子进程数据分离,子进程的数据就存储在另一个物理地址,这时,父子进程这个数据的物理地址不同,页表的映射关系也要改变
父子进程访问同一个变量,通过相同的虚拟地址,经过父子进程页表的映射,找到不同的物理地址,拿到不同的值
代码独立:
父子进程的代码是共享的,但是如果父进程退出,而子进程不退出,依旧需要代码,代码不会释放,所以保证了代码的独立
改变数据前:
改变数据后:
同一个变量,为什么会有不同的值?
现在,就可以解决这个问题
id
是属于父进程进程栈空间中定义的变量,fork内部return会被执行两次,谁先被返回,那这个数据就被修改了,就需要发生写时拷贝,所以,一个变量,会有不同的值,本质是因为大家虚拟地址一样,但是映射后的物理地址不同
为什么要有虚拟地址空间?
- 保护内存
- 进程管理,Linux的内存管理通过地址空间进行功能模块的解耦
- 简化进程本身的设计与实现
补充:页表中堆区的映射关系是在运行时建立的
据时,父进程和子进程的数据对应的虚拟地址相同,页表的映射关系相同,父子进程同一个数据映射到同一个物理地址
但是,如果对数据进行改变,就会放生写时拷贝,将父子进程数据分离,子进程的数据就存储在另一个物理地址,这时,父子进程这个数据的物理地址不同,页表的映射关系也要改变
父子进程访问同一个变量,通过相同的虚拟地址,经过父子进程页表的映射,找到不同的物理地址,拿到不同的值
代码独立:
父子进程的代码是共享的,但是如果父进程退出,而子进程不退出,依旧需要代码,代码不会释放,所以保证了代码的独立
改变数据前:
[外链图片转存中…(img-GWhNWo1r-1662258199214)]
改变数据后:
[外链图片转存中…(img-kpWjBHxX-1662258199214)]
同一个变量,为什么会有不同的值?
现在,就可以解决这个问题
[外链图片转存中…(img-JEabystE-1662258199215)]
id
是属于父进程进程栈空间中定义的变量,fork内部return会被执行两次,谁先被返回,那这个数据就被修改了,就需要发生写时拷贝,所以,一个变量,会有不同的值,本质是因为大家虚拟地址一样,但是映射后的物理地址不同
为什么要有虚拟地址空间?
- 保护内存
- 进程管理,Linux的内存管理通过地址空间进行功能模块的解耦
- 简化进程本身的设计与实现
补充:页表中堆区的映射关系是在运行时建立的