面试加油站 —— 第六期(​Linux操作系统底层基础知识扫盲)

Linux操作系统底层基础知识扫盲

重点知识点(大纲):

  • 内核的五大核心功能:CPU调度、内存管理、文件系统、应用管理(进程调度)、中断管理(设备驱动)

  • 内核分类:宏内核、微内核

  • 操作系统的权限控制 —— Ring环

  • 进程、线程、纤程的关系

    • 进程:操作系统资源分配的基本单位;当创建一个新进程时候,是fork()它的父进程,然后执行exec(),进程树的根为(init/systemd进程)

    • 线程:操作系统CPU调度的基本单位;线程的本质也是一个进程,当创建一个新线程时候,是从当前进程fork()出一个子进程;所有的线程共享所属进程的内存资源;启动新线程大概需要1M内存;

    • 纤程:是比线程更小的运行单位,直接跑在用户空间,相当于是由用户态程序自己实现的一个小的操作系统,当需要新增纤程时,完全不需要劳驾内核;启动新纤程大概只需要4K内存;

  • 僵尸进程 和 孤儿进程(对照记忆)

    • 僵尸进程(zombie):子进程已退出,但是父进程还没有通过wait()或者waitpid()获取它的返回值,则子进程的进程描述符依然存在于系统中,这时候他就是僵尸进程;

    • 孤儿进程:父进程已经退出,子进程还没有退出,那么这时候子进程相当于没有了父亲,就变成了孤儿进程,会变成1#进程的孩子,由1#进程进行维护;

  • 进程调度分类:抢占式(交给进程调度器强制安排)、非抢占式(除非主动让出,否则一直运行)

  • 进程调度策略:Unix O(0)调度策略(看似公平,其实有可能浪费)、CFS完全公平调度策略(根据优先级,调整时间片比例)

  • Linux默认的调度策略(实时进程、普通进程 不一样)

    • 实时进程(急诊):最急的急诊(SCHED_FIFO)、普通的急诊(SCHED_RR)

    • 普通进程(普通门诊):CFS完全公平调度策略

  • 中断:CPU在执行正常程序,突然有个急事,放下手中的事,保存好现场,去处理紧急的事,处理完再回到断点处,恢复现场,继续“被打断”的工作;

    • 硬中断:一般由设备触发,直接中断CPU,CPU再把中断请求交给Kernel内核去处理;

    • 软终端:一般由用户进程触发,int0x80中断,直接中断内核,通过系统调用system_call完成,对应的系统调用表:

      https://blog.csdn.net/shuzishij/article/details/87005219

  • 我们Java进程中一个简单的读磁盘操作的过程:

    Java读磁盘操作 ——> JVM的read()方法 ——> C库的read()方法 ——> (用户态切换内核态)Kernel执行system_call系统调用 ——> 具体的系统调用 system_read()


一、Linux内核Kernel

1、到底什么是内核?它到底做些什么事情?

内核时一个操作系统的核心,用于管理系统资源,提供对软件层面的抽象(例如对进程、文件系统、同步、内存、网络协议等对象的操作和权限控制),和对硬件层面的抽象(例如磁盘、显示器、网卡等)。

内核要做的事情,主要有以下5类:

  • CPU调度

  • 内存管理

  • 文件系统

  • 应用管理、进程调度

  • 中断处理、设备驱动

2、内核的分类:

我们常见两种内核分类,他们主要时架构不同:宏内核 + 微内核

  • 宏内核:Linux操作系统的几乎所有的功能都会在内核中实现;

image.png

  • 微内核:在内核中只保留必须在内核态运行的功能,而将其它功能都使用独立进程去实现,这样大大减小了内核本身的体积,同时各部件升级替换也变得非常的容易。

image.png

我们常见的内核几乎都是”微内核“ —— 所有的几大功能都集成在一个内核中 —— 传统操作系统

”微内核“ —— 将传统内核的几大功能拆分为多个服务,此时的Kernel只有调度功能 —— 华为鸿蒙系统 —— 5G,IOT领域(需要内核足够的小)

3、操作系统对于权限的控制实现——Ring环:

image.png

Intel的CPU将特权级别分为4个级别:ring0,ring1,ring2,ring3,

而现在,无论是Window,还是Linux操作系统,都只是用了ring0(内核态)和ring3(用户态)这两个级别!

  • 操作系统内核的代码运行在ring0级别上:可以使用特权指令、控制中断、修改页表访问设备等;

  • 而用户写的应用程序就只能跑在级别最低的ring3上,不能直接执行受控操作;如果位于ring3及级别的普通应用程序企图直接执行ring0级别的指令,则会直接报错”非法指令“

那如果我们的应用程序,需求就要执行一些ring0级别的操作呢?比如读写磁盘、发送文件怎么办?

        首先,作为JAVA开发人员,我们得认清一点,整个JVM虚拟机也都是泡在ring3级别的,在操作系统和内核面前,JVM也不过就是一个普通的程序而已

        内核程序既然既然是泡在ring0级别的,可以调用ring0级别的指令,那么内核程序的开发者,他们就将这些ring0级别的指令进行了一层封装,然后对下暴露接口,来允许ring3级别用户态的程序去申请调用,如:read、write、sendfile、pthread等;

        这样我们就理解了,为什么我们在JAVA程序中执行一个简单的读盘操作,需要在 用户态 <——> 内核态 之间来回的切换了,这种切换时很消耗性能的,所以我们一般避免去做!


二、进程、线程、纤程(协程)

1、进程和纤程的区别?

  • 进程:一个程序运行起来的状态,是OS分配资源的基本单位;分配独立的内存空间 —— 如JVM启动的时候会从OS申请一块自己独立的内存空间;

  • 纤程:一个进程中不同的执行路径,是OS执行CPU调度的基本单位;—— 一个进程中所有的纤程共享该进程申请的内存空间,线程并没有自己独立的内存空间;

2、线程在Linux操作系统中的实现(线程本质也是进程)?

其实在Linux操作系统中,一个线程就是一个普通进程,只不过与其它线程共享它所属的线程的资源(内存空间、全局数据等)

一个线程就是从当前进程中Fork出一个子进程出来,所以本质也是一个进程!

在其它操作系统(如windows)中,都有各自的所谓的LWP(Light Weight Process)的实现!

3、线程与纤程的区别?

  • 线程:操作系统进行管理的,每新增一个线程,都需要申请在内核态执行,跑在OS操作系统层面

  • 纤程(Fiber):比线程更小的一个运行单位,跑在用户空间,相当于用户态程序自己实现了一个小的操作系统

    • 通常情况下,多个Fiber共享一个固定的线程,然后他们通过互相主动切换到其它Fiber来交出线程的执行权,各个子任务之间的关系非常强!

    • 每当需要新增一个Fiber,只需要我们用户态程序即可完成,不需要切换到内核态,所以性能非常高!

image.png

4、纤程Fiber有哪些优势?

  • 纤程非常的轻量级,占用的资源非常少,一个线程启动前后,需要消耗 1M 左右的内存,而一个纤程启动前后大概只需要消耗 4K 左右的内存空间!

  • 纤程切换不需要执行 用户态 <——> 内核态 之间的切换

  • 因为非常轻量级,所以我们的程序可以启动很多很多的纤程!(10w+都没有问题

5、目前支持纤程Fiber的语言有哪些?

Kotlin,Scale、Go、Python+类库lib、Java+类库lib;

所以说,Go语言比Java有优势,主要就是它原生支持了Fiber!

6、Java中如果想实现纤程Fiber该怎么做?

借助quasar类库:

<dependency>
     <groupId>co.paralleluniverse</groupId>
     <artifactId>quasar-core</artifactId>
     <version>0.8.0</version>
</dependency>

然后我们就可以在代码中使用Fiber了:

@Test
public void testFiber(){
   Fiber<Object> fiber = new Fiber<>(new SuspendableRunnable() {
       @Override
       public void run() throws SuspendExecution, InterruptedException {
           doTest();
       }
   });
   fiber.start();
}

可以看到,纤程Fiber 和 线程Thread 的使用方法是非常类似的!

7、最佳实践(多线程 + 多纤程)

我们在使用多线程的时候,很清楚,我们可以申请的最佳线程数是受机器的影响的,线程数不是越多越好的,根据是IO密集型、计算密集型来设置经验最优的的线程数。

比如:我们有10000个任务需要被执行,那么我们就可以使用 10个线程,然后再为每个线程开启10个纤程,这样的话我们就有了100个纤程,每个纤程只需要执行100个任务即可!

纤程适合做一些短小简单的任务,效率非常的高!

akka框架为什么比Java中的多线程快得多,可能就是其中用到了纤程、或者类似纤程的技术!


三、Linux操作系统中的进程

1、在Linux系统中,如何启动一个进程?

首先我们得要明白,Linux中的每一个进程都存在于一个“进程树”中,每一个进程都有自己的父进程(init/systemd进程除外,init/systemd为树根,进程号为1),我们可以使用pstree进行查看:

# 如果pstree命令不存在,则安装下:
yum -y install psmisc

[root@i-ieseuclw ~]# pstree
systemd─┬─NetworkManager─┬─dhclient
        │                └─2*[{NetworkManager}]
        ├─acpid
        ├─2*[agetty]
        ├─auditd───{auditd}
        ├─chronyd
        ├─crond
        ├─dbus-daemon
        ├─gapd───{gapd}
        ├─master─┬─pickup
        │        └─qmgr
        ├─polkitd───6*[{polkitd}]
        ├─rsyslogd───2*[{rsyslogd}]
        ├─sshd─┬─sshd───bash───pstree  ##我们在这,打开的第一个窗口
        │      └─sshd───bash───watch   ## 我们在这,打开的第二个窗口
        ├─systemd-journal
        ├─systemd-logind
        ├─systemd-udevd
        └─tuned───4*[{tuned}]

那么,当我们需要创建一个进程的时候(以watch为例),就是简单地创建一个“watch”进程就ok了么?其实不是的:

  • 0:首先,Linux操作系统会根据我们当前进程,比如我用ssh连接的,那么我(me)当前的进程就是 sshd —— bash: 

my parent
    |- me
  • 1:然后,操作系统会通过调用系统函数 fork(),基于 me 克隆出一个 子进程:

my parent
    |- me
       |-- clone of me
  • 2:再然后,让我们的子进程执行 exec("watch") ,这样就变成了:

my parent
    |- me
       |-- watch
  • 3:最后,当程序执行结束后,我的进程会变成僵尸进程(zombie)

my parent
    |- me
       |-- watch (zombie)

不过这个zombie进程,我们没办法通过pstree看到:这个僵尸进程其实已经死了,但是它还在等它的父进程,以防父进程需要检查它的返回值<使用wait()/waitpid()系统调用>,一旦我获得了它的返回值,我将再次恢复独自一人的状态:

my parent
    |- me

由此可见:

  • 当Linux需要开启一个新进程执行任务的时候,是通过 系统函数 fork() + exec() 完成的;

  • 而 fork() 函数底层又是调用的 clone() 函数;

image.png

2、上面说init/systemd进程为树根,进程号为1,可是pstree看到的为什么是systemd?

  • init进程:

# 以前的Linux系统启动,都是靠init进程:
/etc/init.d/apache2 start

# 但是init进程有缺点:启动时间长(串行启动),启动脚本复杂
  • systemd进程:在较新的Linux系统上,都使用systemd代替了init,成为了系统的第一个进程(树根PID=1),systemd为系统的启动何管理提供了完整的解决方案,字母d为守护线程(daemon)的缩写;

# 查看systemd的版本:
[root@i-ieseuclw ~]# systemctl --version
systemd 219

# systemd兼容了init,功能强大,使用方便,且启动速度快(并行启动)

3、什么是僵尸进程?什么又是孤儿进程?

  • 僵尸进程:如果一个子进程退出了,但是它的父进程还没有释放它的PCB,那么该子进程的进程描述符就仍然存在于系统中;

    • 直到它的父进程通过调用wait()或者waitpid(),获取了该子进程的返回值该子进程的才会彻底从系统中消失

ps -ef | grep defuct
  • 孤儿进程:与僵尸进程有点相反,父进程已经退出了,而子进程却还没有退出,这时候,该子进程相当于就没有了父亲,那么它将变成1号进程(init/systemd)的孩子,由1号进程进行维护

./orphan

正常时候,僵尸进程 和 孤儿进程,对系统的影响不大!

4、进程调度(内核Kernel五大功能之一)

每个进程都有自己专属的调度方案,且是可以自定义的!

  • 进程调度可以分为:非抢占式(除非主动,否则一直运行)、抢占式(由进程调度器来强制安排):

image.png

  • 调度策略有:经典Unix(0) 调度策略(看似公平,其实有可能浪费)、CFS完全公平调度策略(根据优先级,调整时间片比例):

image.png

5、进程调度的基本概念:

image.png

6、Linux系统默认的调度策略:

  • 实时进程:急诊:最急的急诊(SCHED_FIFO)、普通急诊(SCHED_RR)

  • 普通进程:普通门诊:完全公平调度策略:CFS

image.png


四、中断(非常重要,与用户程序密切相关)

image.png

1、中断的定义?

        CPU在正常处理程序指令的过程中,突然收到来自系统外部、系统内部、或者现行程序本身的紧急事件,此时CPU需要立即暂停正在执行的程序,保存现场后立即转去执行相应的处理程序处理完该事件后再返回断点处继续执行刚刚被“打断”的程序,这一整个过程称为“程序中断”。

        一个人在工作时,可能有人敲门进来要签字,他必须停下手上的工作,当他签字完毕后,再回来继续刚刚的工作。

        同理的,计算机也能在正常工作时,响应别的紧急的任务请求,这就是“中断”,计算机在处理完“中断”请求后继续完成后续工作。

2、中断的分类:

  • 硬中断(CPU中断):简单可理解为,硬中断由硬件触发,如磁盘、网卡,键盘,时钟等:

    • 每个设备或者设备集,都有它自己的IRQ(中断请求),CPU可以根据IRQ将相应的请求分发到对应的硬件驱动上;(简单理解为:CPU总得要分得清,这次的中断请求是来自键盘的,还是来自鼠标的)

    • 硬件驱动,通常是内核的一个子程序,而不是一个独立的进程;

    • 硬中断很直接,会直接中断CPU(因为我们键盘或者鼠标等硬件的操作,我们肯定是希望立即被执行到)

    image.png

    • 上图我们看到了两个新名词:上半场 + 下半场:

      • 上半场:中断源 ——> 通过CPU到 ——> Kernel ——> 对应的处理程序的过程;

      • 下半场:应用程序,处理中断信号及参数的过程;

  • 软中断(Kernel中断):软中断的处理非常像硬中断,但是不会直接中断CPU,而是只中断Kernel内核。

    • 也只有当前正在运行的代码(或者进程)才会产生“软中断”;

    • 这种中断通常是需要Kernel内核,为正在运行的程序去做一些事情(比如I/O请求),因为我们知道用户程序在ring3,没有权利去做这些ring0级别的操作;

    • 有一个特殊的“软中断”是Yield调用,它的作用是请求内核调度器去查看是否有其它的进程可以运行!

3、软中断(int0x80)—— 80中断(int是interrupt的意思)

image.png

内核中维护着一个中断向量表:不同的中断号对应着不同的设备,这样当中断发生的时候,内核可以很清楚地判断出,此次中断是由哪个设备发起的,参数该交由哪个程序去处理!

int0x80中断是一个非常特殊的中断,它就是软中断,对应的系统调用system_call

然后80中断下又对应着一堆的系统调用方法,如read(),write(),pthread()等,我们开发的Java程序在需要调用ring0级别指令时,都会通过int0x80中断,来向内核申请帮助

系统调用system_call表:https://blog.csdn.net/shuzishij/article/details/87005219

4、一个简单的Java读磁盘操作,整个触发过程:

Java读磁盘操作 ——> JVM中调用read() ——> C库 read() ——> (用户态转内核态)内核Kernel ——> system_call(系统调用处理程序)——> system_read()

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐