1. 死锁
什么是死锁?
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
产生死锁的原因?
可归结为如下两点:
- 竞争资源
- 系统中的资源可以分为两类:
- 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
- 另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
- 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
- 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁
- 进程间推进顺序非法
- 若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁
- 例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁
死锁产生的4个必要条件
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。互斥条件是指多个线程不能同时使⽤同⼀个资源
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。当线程 A 已经持有了资源 1,⼜想申请资源 2,⽽资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放⾃⼰已经持有的资源 1
- 不可剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 循环等待条件:在发生死锁时,必然存在一个进程–资源的环形链。在死锁发⽣的时候,两个线程获取资源的顺序构成了环形链。
预防死锁:
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
解决死锁的基本方法
1、以确定的顺序获得锁
如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之前获得锁的顺序。按照上面的例子,两个线程获得锁的时序图如下:
如果此时把获得锁的时序改成:
那么死锁就永远不会发生。 针对两个特定的锁,开发者可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,那么死锁也不会发生。
问题变得更加复杂一些,如果此时有多个线程,都在竞争不同的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就无法满足要求了,这个时候开发者可以使用银行家算法,所有的锁都按照特定的顺序获取,同样可以防止死锁的发生,该算法在这里就不再赘述了,有兴趣的可以自行了解一下。
2、超时放弃
当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去然
而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。 还是按照之前的例子,时序图如下:
|
|
|
|
|
|
避免死锁:
使用资源有序分配法,来破环环路等待条件。
-
预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。
因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
-
银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。
-
安全状态是指至少有一个资源分配序列不会导致死锁。
-
当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。
检测死锁
-
首先为每个进程和每个资源指定一个唯一的号码;
-
然后建立资源分配表和进程等待表。
在 Linux 下,我们可以使用
pstack+gdb工具来定位死锁问题。pstack 命令可以显示每个线程的栈跟踪信息(函数调用过程),它的使用方式也很简单,只需要
pstack <pid>就可以了。可以看到,Thread 2 和 Thread 3 一直阻塞获取锁(pthread_mutex_lock)的过程,而且 pstack 多次输出信息都没有变化,那么可能大概率发生了死锁。
解除死锁:
当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
- 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
- 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
2. 用户态和内核态
用户态变为内核态:
-
系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
-
异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
-
外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
3. 堆和栈的区别
堆与栈的区别有:
1、栈由系统自动分配,而堆是人为申请开辟;
2、栈获得的空间较小,而堆获得的空间较大;
3、栈由系统自动分配,速度较快,但程序员是无法控制的. 而堆是由new分配的内存, 一般速度比较慢,而且容易产生内存碎片,不过用起来最方便;
4、栈是连续的空间,而堆是不连续的空间。
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。比喻很形象,说的很通俗易懂,不知道你是否有点收获。
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可 执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈 的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
JVM
|
|
|
|
4. 程序、进程、线程
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
5. 动态库与静态库
静态(函数)库 一般扩展名为(.a或.lib),这类的函数库通常扩展名为libxxx.a或xxx.lib 。 这类库在编译的时候会直接整合到目标程序中,所以利用静态函数库编译成的文件会比较大,这类函数库最大的优点就是编译成功的可执行文件可以独立运行,而不再需要向外部要求读取函数库的内容;但是从升级难易度来看明显没有优势,如果函数库更新,需要重新编译。
动态函数库 动态函数库的扩展名一般为(.so或.dll),这类函数库通常名为libxxx.so或xxx.dll 。 与静态函数库被整个捕捉到程序中不同,动态函数库在编译的时候,在程序里只有一个“指向”的位置而已,也就是说当可执行文件需要使用到函数库的机制时,程序才会去读取函数库来使用;也就是说可执行文件无法单独运行。这样从产品功能升级角度方便升级,只要替换对应动态库即可,不必重新编译整个可执行文件。
静态库特点总结如下:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
动态库特点总结:
- 动态库把对一些库函数的链接载入推迟到程序运行的时期。
- 可以实现进程之间的资源共享。(因此动态库也称为共享库)
- 将一些程序升级变得简单。
- 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显式调用)。
6. 锁-互斥锁、⾃旋锁、读写锁、乐观锁、悲观锁
互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
⾃旋锁加锁失败后,线程会忙等待,直到它拿到锁; 自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap)需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
对于互斥锁加锁失败⽽阻塞的现象,是由操作系统内核实现的。
互斥锁加锁失败时,会从⽤户态陷⼊到内核态,让内核帮我们切换线程,虽然简化了使⽤锁的难度,但是存在⼀定的性能开销成本。
那这个开销成本是什么呢?会有**两次线程上下⽂切换的成本,**线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
如果你能确定被锁住的代码执⾏时间很短,就不应该⽤互斥锁,⽽应该选⽤⾃旋
当加锁失败时,互斥锁⽤「线程切换」来应对,**⾃旋锁则⽤「忙等待」**来应对。
如果**只读取共享资源⽤「读锁」加锁,如果要修改共享资源则⽤「写锁」**加锁。
当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这⼤⼤提⾼了共享资源的访问效率,因为「读锁」是⽤于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
但是,⼀旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,⽽且其他写线程的获取写锁的操作也会被阻塞。
公平读写锁⽐较简单的⼀种⽅式是:⽤队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。
前⾯提到的互斥锁、⾃旋锁、读写锁,都是属于悲观锁。
悲观锁做事⽐较悲观,它认为多线程同时修改共享资源的概率⽐较⾼,于是很容易出现冲突,所以访问共享资源前,先要上锁。
如果多线程同时修改共享资源的概率⽐较低,就可以采⽤乐观锁。
乐观锁做事⽐较乐观,它假定冲突的概率很低,它的⼯作⽅式是:先修改完共享资源,再验证这段时间内有没有发⽣冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
可⻅,乐观锁的⼼态是,不管三七⼆⼗⼀,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫⽆锁编程,比如在线⽂档。
实际上,我们常见的 SVN 和 Git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。
乐观锁虽然去除了加锁解锁的操作,但是⼀旦发⽣冲突,重试的成本⾮常⾼,所以只有在冲突概率⾮常低,且加锁成本⾮常⾼的场景时,才考虑使⽤乐观锁。
7. fork()函数
fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,copy一个进程,包括代码、数据和分配给进程的资源。
也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
fork的实现分为以下两步
1. 复制进程资源 2. 执行该进程
复制进程的资源包括以下几步
- 进程控制块(PCB):包含了进程的基本信息,如进程状态、程序计数器、堆栈指针等。
- 程序体:即代码段、数据段等,表示进程的可执行程序和数据。
- 用户栈:包含了函数调用、局部变量等信息。
- 内核栈:用于处理系统调用和中断时保存现场信息。
- 虚拟内存池:包括进程的地址空间,即进程可以使用的虚拟内存范围。
- 页表:用于将虚拟地址映射到物理地址的数据结构。
除了上述资源外,还有一些其他的资源也会被复制:
- 文件描述符表:指向文件的引用会被复制,但文件本身不会被复制。
- 信号处理设置:父进程设置的信号处理函数和信号屏蔽字会被复制到子进程中。
- 环境变量:父进程的环境变量会被复制到子进程中。
- 资源限制:如文件描述符数、CPU 时间等资源限制会被复制。
进行进程的话就比较简单了,只需要将其加入到就绪队列即可,接下来就等待cpu的调度了。
8. 计算机启动的时候内存做了什么?
内存映射,CPU 地址总线的宽度决定了可访问的内存空间的大小。
比如 16 位的 CPU 地址总线宽度为 20 位,地址范围是 1M。32 位的 CPU 地址总线宽度为 32 位,地址范围是 4G。你可以算算我们现在的 64 位机的地址范围。
内存中划分出了一片一片区域分配给外设,BIOS(基本输入输出系统)
BIOS 更狠,不但其空间被映射到了内存 0xC0000 - 0xFFFFF 位置,其里面的程序还占用了开头的一些区域. BIOS 程序的入口地址也就是开始地址是 0xFFFF0(人家就那么写的)
在你开机的一瞬间,CPU 的 PC 寄存器被强制初始化为 0xFFFF0。如果再说具体些**,CPU 将段基址寄存器 cs 初始化为 0xF000,将偏移地址寄存器 IP 初始化为 0xFFF0**,根据实模式下的最终地址计算规则,将段基址左移 4 位,加上偏移地址,得到最终的物理地址也就是抽象出来的 PC 寄存器地址为 0xFFFF0。
BIOS 被映射到了内存的某个位置,并且开机一瞬间 CPU 强制将自己的 pc 寄存器初始化为 BIOS 程序的入口地址, 跳转到物理地址 0xfe05b 处开始执行
这块代码会检测一些外设信息,并初始化好硬件,建立中断向量表并填写中断例程
BIOS 把控制权转交给排在第一位的存储设备。
所以 BIOS 负责加载了启动区,而启动区又负责加载真正的操作系统内核
- 按下开机键,CPU 将 PC 寄存器的值强制初始化为 0xffff0,这个位置是 BIOS 程序的入口地址(一跳)
- 该入口地址处是一个跳转指令,跳转到 0xfe05b 位置,开始执行(二跳)
- 执行了一些硬件检测工作后,最后一步将启动区内容加载到内存 0x7c00,并跳转到这里(三跳)
- 启动区代码主要是加载操作系统内核,并跳转到加载处(四跳)
①加电→打开电源开关,给主板和内部风扇供电。
②启动引导程序→CPU开始执行存储在ROM BIOS(基本输入输出系统)中的指令。
③开机自检→计算机对系统的主要部件进行诊断测试。
④加载操作系统→计算机将操作系统文件从磁盘读到RAM中。
⑤检查配置文件,定制操作系统的运行环境→读取配置文件,根据用户的设置对操作系统进行定制。
⑥准备读取命令和数据→计算机等待用户输入命令
9. 数据是如何拷贝到网卡的
image-20210707232134178
10.冯诺依曼体系结构
- 计算机处理的数据和指令一律用二进制数表示;
- 指令和数据不加区别混合存储在同一个存储器中;
- 顺序执行程序的每一条指令;
- 计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成。
12. 用户堆栈和系统堆栈
内核栈是属于操作系统空间的一块固定区域,可以用于保存中断现场、保存操作系统子程序间相互调用的参数、返回值等。
用户栈是属于用户进程空间的一块区域,用户保存用户进程子程序间的相互调用的参数、返回值等。
**系统栈(也叫核心栈、内核栈)**是内存中属于操作系统空间的一块区域,其主要用途为:
(1)保存中断现场,对于嵌套中断,被中断程序的现场信息依次压入系统栈,中断返回时逆序弹出;
(2)保存操作系统子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。
用户栈是用户进程空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。
那么为什么不直接用一个栈,何必浪费那么多的空间呢??原因有二:
(1)如果只用系统栈。系统栈一般大小有限,如果中断有16个优先级,那么系统栈一般大小为15(只需保存15个低优先级的中断,另一个高优先级中断处理程序处于运行),但用户程序子程序调用次数可能很多,那样15次子程序调用以后的子程序调用的参数、返回值、返回点以及子程序(函数)的局部变量就不能被保存,用户程序也就无法正常运行了。
(2)如果只用用户栈。我们知道系统程序需要在某种保护下运行,而用户栈在用户空间(即cpu处于用户态,而cpu处于核心态时是受保护的),不能提供相应的保护措施(或相当困难)。
核心程序的工作栈就是当前运行的用户进程的系统栈。每个进程都有自己的用户栈和系统栈。而且系统栈的大小是确定的(取决于系统允许的中断嵌套数量,即中断优先级个数)。
当系统因为系统调用(软中断)或硬件中断,CPU切换到特权工作模式,进程陷入内核态,进程使用的栈也要从用户栈转向系统栈。
从用户态到内核态要两步骤,首先是将用户堆栈地址保存到内核堆栈中,然后将CPU堆栈指针寄存器指向内核堆栈。
当由内核态转向用户态,步骤首先是将内核堆栈中得用户堆栈地址恢复到CPU堆栈指针寄存器中。
13. 电脑32位和64位有什么区别
1、计算能力不同:64位的系统理论上比32位系统快一倍,并且它们的内存寻址也不一样。
2、支持的最大运行内存不同:32位的电脑最大只支持4G(一般情况只能用3.25G左右),而64位的电脑则可以支持128G甚至更大。
3、运行的软件不同:32位的电脑只能运行32位的软件,而64位的电脑可以运行32位的软件也可以运行64位的软件。
4、支持的系统不同:32位电脑支持32位的系统,而64位的电脑支持支持32位的系统也支持64位的系统。
64 位相比 32 位 CPU 的优势在哪吗?64 位 CPU 的计算性能一定比 32 位 CPU 高很多吗?
64 位相比 32 位 CPU 的优势主要体现在两个方面:
-
64 位 CPU 可以一次计算超过 32 位的数字,而 32 位 CPU 如果要计算超过 32 位的数字,要分多步骤进行计算,效率就没那么高。
但是大部分应用程序很少会计算那么大的数字,所以只有运算大数字的时候,64 位 CPU 的优势才能体现出来,否则和 32 位 CPU 的计算性能相差不大。
-
64 位 CPU 可以寻址更大的内存空间,32 位 CPU 最大的寻址地址是 4G,即使你加了 8G 大小的内存,也还是只能寻址到 4G,而 64 位 CPU 最大寻址地址是
2^64,远超于 32 位 CPU 最大寻址地址的2^32。
你知道软件的 32 位和 64 位之间的区别吗?再来 32 位的操作系统可以运行在 64 位的电脑上吗?64 位的操作系统可以运行在 32 位的电脑上吗?如果不行,原因是什么?
64 位和 32 位软件,实际上代表指令是 64 位还是 32 位的:
- 如果 32 位指令在 64 位机器上执行,需要一套兼容机制,就可以做到兼容运行了。但是如果 64 位指令在 32 位机器上执行,就比较困难了,因为 32 位的寄存器存不下 64 位的指令;
- 操作系统其实也是一种程序,我们也会看到操作系统会分成 32 位操作系统、64 位操作系统,其代表意义就是操作系统中程序的指令是多少位,比如 64 位操作系统,指令也就是 64 位,因此不能装在 32 位机器上。
总之,硬件的 64 位和 32 位指的是 CPU 的位宽,软件的 64 位和 32 位指的是指令的位宽。
14. CPU 执行程序的过程
- 第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的**「控制单元」操作「地址总线」指定需要访问的内存地址**,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」。
- 第二步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给**「逻辑运算单元」运算**;如果是存储类型的指令,则交由「控制单元」执行;
- 第三步,CPU 执行完指令后,「程序计数器」的值自增,表示指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此「程序计数器」的值会自增 4;
一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。
现代大多数 CPU 都使用来流水线的方式来执行指令,所谓的流水线就是把一个任务拆分成多个小任务,于是一条指令通常分为 4 个阶段,称为 4 级流水
四个阶段的具体含义:
- CPU 通过程序计数器读取对应内存地址的指令,这个部分称为 Fetch(取得指令);
- CPU 对指令进行解码,这个部分称为 Decode(指令译码);
- CPU 执行指令,这个部分称为 Execution(执行指令);
- CPU 将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为 Store(数据回写);
上面这 4 个阶段,我们称为指令周期(Instrution Cycle),CPU 的工作就是一个周期接着一个周期,周而复始。
15. Linux内核 vs Windows内核
Linux 内核设计的理念主要有这几个点:
- MutiTask,多任务
- SMP,对称多处理
- ELF,可执行文件链接格式
- Monolithic Kernel,宏内核
MutiTask 的意思是多任务,代表着 Linux 是一个多任务的操作系统。
多任务意味着可以有多个任务同时执行,这里的**「同时」可以是并发或并行:**
- 对于单核 CPU 时,可以让每个任务执行一小段时间,时间到就切换另外一个任务,从宏观角度看,一段时间内执行了多个任务,这被称为并发。
- 对于多核 CPU 时,多个任务可以同时被不同核心的 CPU 同时执行,这被称为并行。
SMP 的意思是对称多处理,代表着每个 CPU 的地位是相等的,对资源的使用权限也是相同的,多个 CPU 共享同一个内存,每个 CPU 都可以访问完整的内存和硬件资源。
这个特点决定了 Linux 操作系统不会有某个 CPU 单独服务应用程序或内核程序,而是每个程序都可以被分配到任意一个 CPU 上被执行。
ELF 文件
ELF(Executable and Linkable Format,可执行和可链接格式)是一种用于存储可执行程序、共享库、目标代码和核心转储文件的标准文件格式。ELF 文件是在类 Unix 系统中广泛使用的一种二进制文件格式,用于表示可执行程序和库。
ELF 文件包含了以下几个主要部分:
- ELF 文件头:包含了描述文件类型、目标体系结构、入口点地址等信息的结构。
- 程序头表:描述了可执行文件的段(segment)和段在内存中的加载位置、大小等信息。
- 节区头表:描述了文件中各个节(section)的信息,如代码段、数据段、符号表等。
- 节区:包含了程序的实际数据和代码,如可执行代码、全局变量、字符串等。
- 符号表:包含了程序中定义和引用的符号(如变量、函数名)的信息,用于链接时的符号解析。
ELF 文件的优点包括了灵活性和可扩展性,它能够支持多种目标体系结构和操作系统,同时也支持调试信息和动态链接等特性。由于这些优点,ELF 成为了现代 Unix 和类 Unix 系统中标准的二进制文件格式。
Monolithic Kernel
Monolithic Kernel 的意思是宏内核,Linux 内核架构就是宏内核,意味着 Linux 的内核是一个完整的可执行程序,且拥有最高的权限。
宏内核的特征是系统内核的所有模块,比如进程调度、内存管理、文件系统、设备驱动等,都运行在内核态。
Windows 和 Linux 一样,同样支持 MutiTask 和 SMP,但不同的是,Window 的内核设计是混合型内核,
对于内核的架构一般有这三种类型:
- 宏内核,包含多个模块,整个内核像一个完整的程序;
- 微内核,有一个最小版本的内核,一些模块和服务则由用户态管理;
- 混合内核,是宏内核和微内核的结合体,内核中抽象出了微内核的概念,也就是内核中会有一个小型的内核,其他模块就在这个基础上搭建,整个内核是个完整的程序;
Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用了混合内核。
这两个操作系统的可执行文件格式也不一样, Linux 可执行文件格式叫作 ELF,Windows 可执行文件格式叫作 PE。
16 守护进程 孤儿进程 僵尸进程
守护进程
守护进程指在后台运行的,没有控制终端与之相连的进程。
它独立于控制终端,周期性地执行某种任务。
Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等,udevd负责维护/dev目录下的设备文件 , acpid负责电源管理,syslogd负责维护/var/log下的日志文件,
可以看出守护进程通常采用以d结尾的名字,表示Daemon。
精灵进程作用:提供服务。eg:内核线程:完成操作系统级别服务.
创建守护进程要点:
(1)让程序在后台执行。方法是调用fork()产生一个子进程,然后使父进程退出。
(2)调用setsid()创建一个新对话期。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,
方法是调用setsid()使进程成为一个会话组长。setsid()调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。
(3)禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过fork()创建新的子进程,使调用fork的进程退出。
**(4)关闭不再需要的文件描述符。**子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。
(5)将当前目录更改为根目录。
(6)子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。
(7)处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。
如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。
孤儿进程
如果父进程先退出,子进程还没退出,那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)。
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程
如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。
设置僵尸进程的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。
如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。
如何避免僵尸进程?
通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂
起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该
信号是子进程退出的时候向父进程发送的。
父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。
如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。
通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。
第一种方法忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源
————————————————
1. 程序、进程、线程概念分别是什么。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是资源分配的基本单位,它是程序执行时的一个实例,在程序运行时创建;
线程是程序执行的最小单位,是进程的子任务,是进程的一个执行流,一个线程由多个线程组成的
具体来说:
- 进程是操作系统分配资源的单位,而线程是进程的一个实体,是CPU调度和分派的基本单位。
- 线程没有独立的内存单元,不能够独立执行,必须依存在应用程序中。
- 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。