1. 冯诺依曼体系
日常的、不日常的所有设备都遵守冯诺依曼体系结构。
•输⼊单元:包括键盘, ⿏标,扫描仪, 写板等•中央处理器(CPU):含有运算器和控制器等•输出单元:显⽰器,打印机等
所谓存储器,就是内存;常说的外存,其实是 磁盘
CPU在数据层面,不会和设备打交道,而都是和内存进行交互(不考虑缓存)。
那么电脑上的程序是怎么执行的呢?
二进制程序(代码和数据一起构成一个二进制程序)是一个文件,文件是从磁盘中来的。
而“加载”又是什么呢?
就是从磁盘将文件转移到cpu,转移的本质就是拷贝,因此设备拷贝效率本质就是计算机的效率。
2. 举例,Input & Output
输入与输出不是简单的scanf与printf,这是很表层的现象。
scanf和printf作为代码被编译成可执行程序,程序要运行需要被加载到内存。scanf和printf被编译成能被机器识别的机器码,然后这些机器码调用了相应的接口,然后scanf和printf对应的这些字段才能与键盘或者显示器进行交互。
再比如QQ发消息:
而不是:
因为外设的速度慢,根据木桶原理,这样整个计算机的速度都很慢。但是如果都用cpu那样的芯片去构建整个机器,价格又会很高,所以说内存的设计————cpu和外设之间的巨大缓存,是一个伟大的发明,让大部分人能用上电脑,让今天读者和笔者的所作所为有意义。
3. OS
操作系统,operator system
任何计算机系统都包含⼀个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:•内核(进程管理 Process Management ,内存管理 Memory Management ,⽂件管理 File System Management ,驱动管理 Device Management )浅显理解他们是干什么的:内存——卡死
进程——程序挂掉
文件——拍照的照片该放哪
驱动——识别新设备(新的声卡)
•其他程序(例如函数库,shell程序等等)
操作系统的地位:
最底层一定都是硬件(键盘、网卡、声卡等),而每一种硬件几乎都需要自己的驱动程序。
比如录音用的专业声卡,一般都需要下载厂家官网的驱动程序。
设计OS的⽬的:•对下,与硬件交互,管理所有的软硬件资源•对上,为用户程序(应⽤程序)提供⼀个良好的执⾏环境
因此:
你是计算机的主人,负责决策部分,操作系统则负责执行部分
对所有对象的管理都变成对某种数据结构的增删查改
计算机管理硬件1. 描述起来,⽤struct结构体2. 组织起来,⽤链表或其他⾼效的数据结构
所有的OS逻辑都遵循:先描述,后管理!!!!
3.1 system call系统调用与库函数
比如上文的scanf,明明这是一个与外设交互的指令,怎么可能简单在代码层就实现交互了呢?
首先,操作系统为了自身的安全,不允许外部访问操作系统(操作系统不允许任何上层用户访问OS内部!),因此只会提供接口,而这些开放出来的接口就是系统调用
printf与scanf(或者cin cout等),就是对相对应的系统调用接口进行封装之后、存进库函数的指令。有意思的是,所有语言都是在用户层上跑起来的应用程序,他们一定也会调用系统调用。系统调用都是C语言的接口,因此所有软件的底层,都必须和C直接或者间接相关。
顶层管理的再次总结:
先描述,再组织
系统调用进行封装->库
但是比如数学库,不需要使用底层的资源(直接调用的cpu)
关闭窗口就是对显示器的清屏,也是在访问硬件(显示器)。
比如ls,ls一定要去调用底层的磁盘,才能获得相关的名字
4. 进程
4.1 概念
操作系统是计算机开机之后启动的第一个程序
操作系统是怎么管理进⾏进程管理的呢?很简单,先把进程描述起来,再把进程组织起来
因此,进程也是操作系统管理下的一个概念。
比如:磁盘中有一个可执行程序叫myexe,是由code.c编译来的。
运行myexe时,需要先把myexe拷贝到内存中来。
要把可执行程序加载到内存中,但不是只加载可执行程序,还需要加载进程的属性,而进程的属性的另外一个名字就是PCB(process control block)
因此,进程的定义:
PCB的定义:PCB是操作系统用于描述进程的当前状态以及控制进程运行的全部信息的数据结构。它是进程存在的唯一标志,记录了操作系统所需的,用于描述进程的当前情况以及控制进程运行的全部信息,如进程的状态、优先级、程序计数器、内存指针等
4.2 task_struct
task_struct—————PCB的⼀种•在Linux中描述进程的结构体叫做task_struct。task_struct是Linux内核的⼀种数据结构,它会被装载到RAM(内存)⾥并且包含着进程的信息。可能同时会加载很多个进程,这些task_struct可能会以一个链的方式存在于kernel中,这样更便于管理。并且也实现了:一切对象的管理都变成对一种数据结构的增删查改。
同时,一切运行起来的程序都在被cpu调度,或者基于优先级调度
进程被cpu拿上来又放下去、拿上来又放下去,呈现出动态的特征
并且进程在kernel中是以链表形式被组织起来了的
5. 观察进程
运行起来的程序:进程,所以先实现一个可观察的程序:
注意,一定要在printf后面带上/n,否则缓存区不刷新。
查看进程:此处需要再打开一个shell,方便观察。
一个shell执行那个一直打印pid的程序,另一个观察pid
查看语句:
a代表显式所有进程,x代表显式没有控制终端的进程
ps的全称是process status
前面半句(head -1的目的是打印表头)是为了打印表头:
为什么每次查,还会多一个红色的?grep本身也是进程
所以查的时候把自己的(grep也匹配进来了)
可以再反向grep一次
ps ajx | head -1 ; ps ajx | grep test | grep -v grep
观察到进程之后,我们能更好的理解进程了:
比如ls是一种一跑就退的进程;而杀毒软件就是一种常驻进程
6. PID(process Identification)
pid,就是指进程的编号,是task_struct中第一个需要我们认识的属性
同一个程序,在不同的时候启动,pid可能不一样,很正常
如何获得自己的pid
(pid可用于维持进程的唯一性):
每一个执行程序都有自己的进程,getpid用于获得该进程。
getpid
返回类型是pid_t,本质是一个无符号整数
可以在测试代码中打出来(系统封装):
第五大点已经进行过该测试。
如何结束一段进程(除了CTRL+C)
用kill -l查看kill指令的所有指令。
先只使用第九条,single kill(关闭单一进程)
根目录下的proc目录:
进程下的信息全部信息都被存进了这个文件夹,让用户查看
此时执行ls /proc/1
但是,一号是操作系统,普通用户没有权限,出现了几个permission denied
操作我们自己的test:
不难理解,proc目录是实时更新的。
到文件夹里去看:
并且proc文件夹里的属性比直接ps出来的属性丰富得多
磁盘不会存proc,一旦操作系统结束运行(关机),proc会被释放
因此,以上文件其实根本就不存在于磁盘中。
proc的文件都是存在内存中的,(内存级数据,因此实时更新)
可以称之为虚拟出来的文件系统
着重介绍exe和cwd:
exe用于记录该进程是从哪个可执行程序处形成的。
如果在执行test的时候删除源文件,执行的程序依然会进行(这也再次印证了程序是加载到内存之后再开始运行的),但是在该进程对应的proc文件夹下的exe处就会变红并且报错。
cwd:current work dir
进程会记录下可执行程序在启动时所在的路径
所以,“当前路径”,其实是进程的CWD(current work dir)
当前路径是一个全路径。
如果想改进程启动时所对应的CWD属性:
例如:
想chdir到根目录,明显是没有权限的。
但如果不去perror也不会报错。
比如一些程序,为什么会默认先对“当前路径进行放文件”,就是这个原因,如果更改当前路径(chdir),就会在chdir的位置去创建或者下载文件了。
现在chdir到其他目录然后再观察:
小结:
ps比打开proc更常用,但是ps命令本质就是基于proc实现的。
7. PPID
除了ppid(parent pid),还有pgid(process group id)等,先看如何获得ppid:
ppid一直都不变 :
并且command是来自bash。
shell是操作系统内核的统称,其中bash是其中一种shell
并且,每登录一次xshell,就会产生一个bash
自己创建子进程:
使用库函数fork,创建子进程(fork的中文意思是叉子,在叉子的头处是分叉的)
再修改一下测试函数:
对id值进行强转,避免告警
I am a 分支 被执行了两次
其中11218是bash,是31385的父进程;
31385是31386的父进程。
经过fork之后,就有两个执行分支,一个进程要调printf,另一个也需要调用printf
两个分支,是父子关系。
一个父进程可以创建多个子进程,而一个子进程只有唯一的父进程。
因此,进程也是树形结构。
目前只需要停留在bash,不再往上深究。
回过来研究fork函数的返回值:
RETURN VALUE
On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.如果成功,给父进程返回子进程的PID,给子进程返回0
两个死循环,同时在跑
两个分支,if 和else if同时成立
父进程必须拿到每一个子进程的pid,这样才能方便管理。
所以一个fork竟然有两个返回值:
子进程的代码是从父进程那里加载过来的 ,父进程的代码是从磁盘处加载过来的。
代码是共享的,那数据呢:
进程之间具有很强的独立性
⽗⼦进程代码共享,数据各⾃开辟空间,私有⼀份(采⽤写时拷⻉)因此gval的数据会各变各的,并且因为是两个进程,所以父进程中,id接受到的是子进程的PID;子进程中,id接受到的是0,因此两个分支都会被执行
使用C++创建多个进程demo:
配合vector使用。(不同的机器,面对一下多进程代码可能会有不同的反应)
理想结果如下:比如
pid是连续的。
在每一个新的pid被for返回之前,fork函数的实现中会完成包括但不止下列任务(比如加入对应的task_struct,然后调整task_struct的属性,但是这个task_struct还是指向原来的代码),
但是在return之前的瞬间,父、子进程一定已经被调度起来了(函数的大部分功能已经完成才会return),但是父与子进程谁先调度起来是不一定的,由操作系统决定。