Linux系统基本原理(init)

简单了解一些Linux的系统原理(init), 等以后学了操作系统再来补充和更正吧~ 先来简单的了解知道个大概.

操作系统的基本原理(Linux)

先来在重复了无数遍的东西, 一个计算机由硬件做底层向上是: 操作系统(内核 kernel) , 由于直接的系统调用不适合编程, 所以有了诸多的库调用. 而调用就是使用/载入别人写好的模块, 接着为了实现生产功能, 程序员面向这些库进行编程.当然也可以进行混合编程, 面对库和系统调用接口进行编程. 这就是我们静态看到的操作系统的状态.

sys

那么当系统运行起来, 程序在操作系统中跑起来的时候, 这又是什么状态呢.

之前说过, 当进行系统调用的时候CPU根据指令执行工作,而指令又分成不同等级的(普通指令–环3, 特权指令–环0), 一般特权指令都是直接对硬件进行的操作, 如果一个程序可以随便调用, 那岂不是就会大乱 ? 那么如果这些程序需要进行硬件管理的操作怎么办呢? 那就调用系统接口进行实现.

这样指令排成队列一个一个的交给CPU去执行, 当出现了系统调用的时候, 程序就会等待内核将处理的结果交给程序. 那么此时 就要进行模式转换, 这是由程序发起的一次软中断实现的. 我们都知道操作系统划分为用户态和内核态, 或者叫模式吧, 那么在这次切换的时候, 程序运行就会从用户模式 --> 内核模式 .

那么现在来说说内核的功用: 进程管理, 文件系统, 网络功能, 内存管理, 驱动程序, 安全

当用户空间的程序需要用到以上这些功能的时候, 就要向内核发起调用来实现.

这样, 我们的CPU就在执行普通指令和特权指令的过程中来回切换, 这个过程就是上面说的模式切换.

事实上, 一般情况下, CPU是花费了更多的时间在用户模式的, 这因为只有用户模式的程序才具备生产能力.(>=70%)

我们都知道, Linux是可以同时执行多任务的系统. 但是一个单核CPU同时又只能执行一个任务, 这种是怎么实现的呢?

为了将有限的资源用在有着无限欲望的进程中, CPU采取了时间片的策略, 一个进程执行一段时间后就会执行另一个任务, 不管是否执行结束. 这样就会产生一个问题,有一些进程, 例如系统进程, 显然应该分配多点时间, 但一般的程序进程就不要占据太长时间了, 或者说, 多个进程在等待, 我该先走哪一个呢?

这样我们就给进程加了一个优先级的标签, 当然, 一个进程的优先级再高, 他也不能占据着CPU不放 . 因此, 内核使用它进程管理的功能, 如果一个进程占用CPU的时间过长, 内核就会直接将他踢下去, 接着从后面排队的进程中选择一个上来, 这时候的选择仍然是根据优先级, 谁的优先级高谁就被替补上来. \

在把一个进程踢下去的时候, 如果不对当前的进程进行执行上下文的状态进行保存, 那么下一次在再次执行的时候就又要从头再来了. 这些然是不合理的, 所以每一个进程在被切换下去的时候, 中间数据会放在CPU的寄存器中, 而指令指针永远指向下一个要执行的指令. 当再次执行到这个进程的时候, 就会进行恢复现场, 将数据再还回去.

这个切换的过程是需要占用CPU的资源的, 所以当太多时, CPU会浪费时间在切换上, 当然这个时间的消耗是难免的.

简单的理解, 进程是活动的实体, 运行中的程序的一个副本. 而程序就是一个静态的代码, 存在于磁盘上.

所以说进程是存在活动周期的, 内核对每一个进程创建了一个元数据, 存储了这个进程的种种信息. 存储进程的结构体叫做task struct, 这些结构体是以链表的形式存储的,最终形成task list

在上面我们说用户代码是不能进行越权操作的, 但是谁也不能保证用户不会尝试编写这样的危险代码, 所以内核是有着非常严格的审查机制, 在不考虑漏洞的情况下, 任何时刻, 只要出现这样的越权情况出现, 中断被触发, CPU会立即唤醒内核.

行吧, 接下来就来看看怎么创建一个进程

像 上帝造人那样. 上帝并不想要关心人间事务, 于是他就仿造自己创造了亚当. 当系统启动时, 会先执行内核代码, 等待内核启动完毕时也就是内核接管一切的时候, 就由内核进行管理和调度 这就是第一个进程, 也就是过去Linux的总进程init 这个进程来创建子进程, 接着子进程会再创建子进程.

父进程创建子进程, 使用fork()来创建, 这个是Linux的一个系统调用接口. 而fork()之后又会克隆自己的数据到子进程中, 这是另一个系统调用clone(). 也就是说, 在创建的时候, 父进程和子进程共享一个内存空间, 这个时候子进程的数据和父进程是一模一样的, 只要子进程不会修改自己的数据, 他就永远和自己的父进程共享同一个内存空间, 如果一旦出现了子进程修改数据的时候,就会复制一个到另外的内存空间(基于CoW),

当进程被销毁的时候, 永远都是父进程存活而子进程死亡, 也就是白发人送黑发人.

在实际的执行过程中, 子进程正常执行而父进程就会进入睡眠, 直到子进程结束父进程收拾掉子进程的残存资源后就会继续执行自己了

pro

接下来再说说优先级的优化问题.

进程存在优先级, 那么当有一千条进程在后面排队时, 因为在每一个结构体当中可以获取到每一个进程的优先级, 那么假设内核挨一个一个去查看, 就会进行完整的遍历, 那么当进程太多的时候就会变的很卡.

进程优先级:

0-139:

  • 1-99 : 实时优先级 ( 数字越大优先级越高 )
  • 100-139: 静态优先级 ( 数字越小优先级越高 )

为了便于管理, Linux给这个分配了Nice值, 这个值的范围区间是从-20-19, 对应上面的100-139.

上面说过, 一个进程是一个结构体, 那么这个结构体是什么样的呢 ?

taskStr

  • state 状态
  • thread_info 线程信息
  • flags 标志位
  • run_list tasks 任务列表
  • mm 栈内存结构
  • real_parent 真实的父进程
  • tty
  • fs files 文件描述符
  • signal 持有的信号
  • ….

而每一个功能又导向了一些子结构, 所以是十分复杂的.

重要的话先说, 每一个进程所拥有的内存都是虚拟的.

物理内存只有一个, CPU只有一颗, 机器上有n个进程, 而且我们的进程不断的被创建和销毁, 也就意味着有内存不断的被分配和回收. 每一个进程在创建时, 都是内核在进行资源的分配, 由于不同进程对内存的大小是有区别的, 即使是同一个命令, 在数据不同的时候, 他所需要的内存也是不同的, (e.g: cat anaconda-ks.cfg和cat /etc/service 后者会出现明显的卡顿) 同样 内核也不知道该分给进程多少内存. 这个问题是这样解决的:

最初整个内存中一定存在的一段是属于内核的, 接着一段是要分配给其他的进程的:

mem_alloc

为了能够高效的分配内存, OS采取了这样的方法 :

page

将剩下的内存进行分页, 每一个页的大小是4K, 这样一个一个页的分配给进程, 如果不够就再分配,所以这些的进程的内存组成, 其实是一个一个页拼接起来的.接着, 内核将这些伪装成是一个连续的, 大段内存. 实际上呢, 是虚拟的. 不仅如此, 内核还向进程描绘了一个很棒的前景.

fake

每一个进程都以为除开内核就是自己, 其实实际上是用的时候在分配, 你用不到那么多就不给你, 也就是仅分配给所需要的. 每一个进程所分到的空间叫做线性地址空间, 而总共的内存叫做物理地址空间, 他们基本上是离散的对应的关系, 而这种映射关系由内核实现. 这种实现机制就叫做虚拟内存实现机制.

一个进程的内存构成是这样的:

struct

现在就来考虑一个进程的内存不够用了, 那么就要分配更多的内存给他. 内核开始在页中进行扫描, 使用LRU:最近最少使用算法. 一旦发现有页的使用最少, 就先把它挪出来, 当需要的时候就在把他给放回去, 但是这个时候, 回来的页已经不再是原来的那个了, 还记得上面说的进程的task_struct嘛, 这里面存放了映射关系的表, 在每一次进程访问内存的时候, 内核都要做一次转换, 将虚拟的指向物理上的. 这么一个过程由一个CPU上专门的硬件来负责, 叫做mmu : Memory Management Unit . 当一个进程被加载到内存上的时候, 我们就把这个映射关系放到mmu上进行实时的转换. 而进行分配的时候, mmu中的映射关系就要进行重新映射, 这个时候才能访问到真实的内存.

当进程访问的资源不在内存中的时候, 就要向内核发起调用请求从磁盘装载进内存,这个时候就发生了缺页异常.

尽管前面说过, 每一个进程都以为整个内存空间只有自己, 但是我们又需要进行进程间的通信(IPC: Inter Process Communication) 既然进程彼此不知道其他的存在, 那么我们怎么使他们进行通信啊? 如果是同一主机: 我们有多种方法, 共享内存 信号 信号量等等… 如果是不同主机进行进程间通信, rpc: remote procecure call远程进程调用 , socket套接字.

Linux的内核多任务是抢占式的多任务

Linux有多种进程类型:

  • 守护进程: 在系统引导过程中启动的进程, 跟终端无关的进程;
  • 前台进程(交互式): 跟终端有关, 通过终端启动的进程.
    • 注意: 也可以把前台启动的进程送到后台, 以守护模式运行

接下来再来简单提一下 进程的状态

运行态: running 就绪态: ready 睡眠态: sleep ( 分为可中断和不可中断的 ) 停止态: 暂停于内存中 ,但不会被调度, 除非手动启动 僵死态: zombie

最后, 说说进程分类:

  • CPU-Bound cpu密集型
  • IO-Bound io密集型

一般都是交互式的进程偏io密集型的.