硬件,软件和进程
硬件
冯诺依曼体系结构
从控制器发出的都是控制信号,其他的都是数据信号
存储器
存储器是内存,而我们平常用的磁盘其实是输入输出设备,而不是存储器
输入输出设备(统称外设)
由于计算机只能处理二进制数据,所以我们是无法直接访问内存的,所以我们需要输入输出设备来进行转换。
输入设备:鼠标,键盘,摄像机,话筒,磁盘,网卡等
输出设备:显示器,播放器,磁盘,网卡等
中央处理器(CPU)
包括运算器和控制器
运算器:对我们的数据进行计算任务,包括算数运算和逻辑运算
控制器:对我们的计算硬件流程进行一系列的控制
数据流向
从输入设备输入的数据不能直接送到cpu,而是要经过存储器才能送到cpu,同样的,cpu处理完的数据是不能直接送到输出设备,要经过存储器才能送到输出设备
存储金字塔
存储的容量越大,速度越慢,单价越便宜,离cpu越远,存储的容量越小反之
这也就是为什么要有存储器的原因,外设和cpu的速度差太大了,需要存储器来协调速度,并行处理数据,内存的速度适中,容量适中。所以我们可以把它看成一个硬件级别的缓存空间,输入设备可以预加载数据,内存可以先读入数据,输出设备同理,并所以整机的速度就是内存的速度了
总线
各个硬件单元必须用线连接起来,cpu和内存之间交互的线叫做系统总线,cpu和外设之间的线叫做io总线
软件
操作系统
操作系统是一款进行管理的软件,操作系统可以通过对硬件和软件进行管理,最终目标是给用户提供一个稳定良好的运行环境,但操作系统不允许任何用户访问核心数据,只提供接口供用户使用
内核
包括进程管理,内存管理,文件管理,驱动管理
其他程序
包括函数库,shell程序等等
操作系统的管理
操作系统不直接接触硬件程序,而是通过硬件上层的驱动程序进行管理,先对硬件用struct描述(对象),再组织(用各种数据结构组织起来,所以我们所有的管理动作就转换成了对数据结构的增删查改)
库函数
库函数是我们调用的printf等c,c++函数等高级语言的函数。
系统调用是操作系统提供的接口,库函数是系统调用接口封装后的产物
进程
概念
一个加载到内存的程序就是进程/正在运行中的程序叫做进程
进程=内核PCB数据结构对象+对应的代码和数据
管理
一个操作系统可以同时运行多个进程,所以我们要正常运行操作系统的时候,需要把所有的进程组织起来管理,核心思路也是先描述后组织
PCB
概念
任何一个进程在创建,加载到内存的时候,操作系统要先把相应的代码和数据加载到内存里,再描述进程的结构体对象,这个对象我们叫做process ctrl bar(PCB)–进程控制块,也是进程属性的集合,这样才是真正创建一个进程,通过PCB就可以对代码和数据进行管理,在Linux中的PCB叫做task struct,它被装载到RAM(内存)内,在Linux中最基本的数据组织形式是双向链表,一个PCB可以同时属于某个链表,某个队列,错综复杂
task_struct的内容
标识符:用于区别其他进程
也就是下图的pid
getpid
谁调用这个函数,getpid会返回这个进程的task_struct里的pid值,pid_t是一个有符号整数,在一个进程里,pid是不变的
getppid
获得当前进程的父进程的pid
而且我们多运行几次会发现父进程的pid每次都一样,我们可以看到这个进程是bash,所以我们以后运行的所有进程其实都是bash进程的子进程,但其实在重启xshell的时候,bash进程的pid是会变的
ps
ps ajx | grep process
如果想查看相应属性可以使用以下命令
ps ajx | head -1 && ps ajx | grep process
如果不想把grep进程也包含进来,可以使用以下的指令,-v表示反向匹配
ps ajx | head -1 && ps ajx | grep process | grep -v grep
用以下的指令可以查看所有进程,也可以查看上一张图片的pid文件夹
ls /proc
这些蓝色的目录其实是包含了进程的大部分属性,我们可以看见process进程的pid为22978,而且我们科打开这个目录
当我们结束进程之后,我们可以看见这个文件不见了
而且我们如果再次运行process的时候,进程的pid大概率是会发生变化的,可以看到下图进程的pid改变了
cwd(current work dir):当前进程工作目录,在文件中调用fopen的时候,就会在当前路径下查找或者创建文件,原因就是进程在当前的工作目录
exe:链接文件,当前正在运行的进程
其他概念
2.状态:任务状态
3.优先级:相对于其他进程的优先级
4.程序计数器:程序下一条指令的地址
5.内存指针:程序代码和相关数据的指针,还包括其他共享内存块的指针
6.上下文数据:进程执行时寄存器的数据
7.I/O状态信息:包括I/O请求,分配给I/O设备和进程使用的文件列表
8.记账信息:时间周期数总和,也就是某一个进程在CPU上运行多长时间
9.其他信息
创建进程
fork
fork函数可以用来创建进程,创建一个进程后,执行流就会多加一个,
如上图,在两个printf函数中调用fork,我们会发现fork后面的代码被执行了两次
fork成功了返回子进程的pid给父进程,把0返回给子进程,如果失败了返回值为-1,难道返回值有两个吗?
我们可以写一个代码运行一下,验证,我们会发现,会同时走if和else if两个分支,而且两个分支里面的死循环在同时跑
我们可以看到当我们还没开始调用fork()的时候,当前有一个进程,pid为1638,ppid为32519,而fork完后,出现了一个子进程,pid为1639,ppid为1638,和当前进程一起执行,其实是就是出现两个分支,fork给父进程返回大于0,给子进程返回0,父进程子进程同时执行,,一般而言,fork以后的代码父子进程共享,数据不共享,返回不同的返回值是为了区分不同的执行流,让父子进程执行不同的代码块,子进程的task_struct其实是新创建的,但只修改了一部分结构体数据,比如说pid和ppid,而且子进程没有自己的代码,所以子进程和父进程共享代码
1.关于fork为什么能返回两次:走到fork函数里面,函数体创建进程后才会返回值,所以执行返回值的时候,其实已经创建完子进程了,所以才能出现”返回两次“的错觉
2.关于父子进程共享代码,所以一个id里为什么会有两个不同的内容呢:对于父进程的数据,子进程不一定全部访问,所以不能把父进程的数据全部拷贝,子进程如果要访问父进程的数据并修改的时候,操作系统会开一块空间给子进程,所以刚开始父子进程的数据和代码是共享的,到后面才慢慢申请,进行数据层面的写时拷贝
3.关于父子进程谁先运行:是不确定的,只有调度器知道,用户是管不了的
进程状态
操作系统学科里的进程概念:
运行状态
每一个cpu都有一个struct runqueue(运行队列),这样就可以把所有需要运行的进程连接起来,每次需要运行进程的时候,直接把head对应的task_struct拿去执行即可,凡是处于运行队列的进程都是运行态,也就是R状态
struct runqueue
{struct task_struct* head;struct task_struct* tail;
};
关于时间片:如果我们写一个while死循环,那我们的这个进程会一直执行吗?答案是不会的,当只有一个cpu的状态下,我们的进程并不是一直占用cpu资源,而是会按照时间片分配cpu资源,每一个进程都有一个时间片,所以最终在一个时间段内,我们所有的进程都会被执行,也就是并发执行,所以cpu上会存在大量的把进程从cpu上拿下来和放上去的动作,也就是进程切换
以上所说的都是对软件做管理,但对于外设也是一样的,都是先描述,后组织
阻塞状态
当我们需要从外设里读取数据,比如说我们使用scanf读取数据,这个进程会链入键盘外设的pcb后,但我们一直没有输入,这个进程就会被阻塞,直到获得了数据,操作系统会把这个进程放入run queue里,才会变成运行状态,当操作系统进程资源严重不足的时候,会把在阻塞状态的进程的代码和数据换到外存,这个进程就变成挂起状态,把需要运行的代码和数据换进来
挂起状态
当我们需要从外设里读取数据,比如说我们使用scanf读取数据,如果我们一直不输入,操作系统的内存却严重不足的时候,操作系统会保留此进程的PCB,然后把进程的代码和数据放到外设中,等需要的时候再载入到内存,省出来的内存就可以给其他进程使用,这就是换入换出,一般挂起进程的都是阻塞的
具体Linux里的状态
正在跑的死循环一定是r状态吗?实则不然
#include<stdio.h>int main()
{while(1){printf("xxxxx\n"); }return 0;
}
但是,一定不是r状态吗?其实也有可能的,但我们查到的大部分都是S状态,因为CPU速度太快了
#include<stdio.h>int main()
{while(1);return 0;
}
实际linux的状态类别
还有‘t’(tracing stop)和‘Z’(Zombie)状态
S状态:浅度睡眠,随时可以响应外部的反应
D状态:深度睡眠,不相应任何外部的请求,进程不能被杀掉,但如果操作系统里的D状态太多了,就会出问题,如果我们想看一下D状态,可以使用dd命令
挂起状态:我们是感受不到的
T和t状态:暂停状态,gdb调试器就是一个t状态,目前来看T和t状态差不多
接下来是T状态
可以看到19号是一个sigstop
僵尸状态
Z状态:把进程放入垃圾回收站,也就是僵尸进程状态,要让父进程等关心这个进程读取完数据后,才会变成X状态,下图的defunct表示“死亡”,当父进程一直没有主动回收这个子进程信息,子进程会一直让自己处于Z状态,进程的相关资源,特别是task_struct不能被释放,此时就造成内存泄漏问题,可以让父进程用waitpid解决
危害:
1.进程的退出状态必须一直保持,因为此进程的父进程要知道这个进程的状态
2.维护退出状态意味着要一直维护task_struct
3.造成内存资源的浪费
4.内存泄漏
孤儿状态
当父进程先退出,却没有回收子进程的时候,子进程的父进程会改成一号进程,即我们的操作系统,此时这个进程被称为孤儿进程,称该进程被系统领养
进程优先级
ps:如果想在vi注释,可以ctrl+v,hjkl选中,shift+i,输入一个//,最后esc
如果想取消,可以ctrl+v,hjkl选中,在输入一个d就可以取消了
ps -l
由于在两台中其中一台使用用ps -l没有办法看到另外一台的进程信息,必须要用ps -al,下图的PRI就是优先级,数值越小越早被执行,NI是进程优先级的修正数据,也叫当前进程的nice值,PRI(new)=PRI(old)+nice,但是NI的范围有限,是-20~19,用户不能过分调整进程优先级
top
按下r键,在这里有一个
可以在下图里修改进程的NI,但如果我们是普通用户去调整优先级的话,是不让你调整的,所以我们可以使用su -调整成root用户
这边我把NI修改为-20,即使我们写了-30,最后也会变成-20,下图的PRI也变成了60,注意,Linux优先级的调整,每一次的优先级的基准值都是80
这里有一个小知识点:Linux内核里O(1)算法调度不同优先级的进程