Linux发行版本 = 启动引导程序 + 内核 + 驱动 + 应用软件

Linux基本特性

  • 单内核(宏内核):作为一个单独的大过程来实现,所有内核服务运行在一个单独的大内核地址空间。相较微内核而言,具有简单和性能高的特点(直接函数调用,少了IPC的开销)
  • 抢占式操作系统:由调度程序决定什么时候停止挂起一个进程的运行,以便其他进程得到执行机会。(用户抢占、内核抢占)
  • 支持多用户、多任务、多线程、多CPU(SMP)
  • 模块化设计:通过kconfig配置内核
  • 可动态加载内核模块:insmod/rmod ko
  • 万物皆文件:系统上的几乎所有资源都以文件的形式来表示,通过统一的操作接口(open/close/read/write/ioctl )来进行访问
  • Unix基因:借鉴了Unix的许多设计并实现了Unix的API(由POSIX标准和其他Unix规范所定义)

Linux启动

image-20210723101212012

Linux文件系统

  • 虚拟文件系统(VFS):对不同的文件系统做一个抽象,提供统一的API访问接口
    • 嵌入式Linux应用中,主要的存储设备为RAM和FLASH
    • 常用的基于存储设备的文件系统类型包括:jffs2, yaffs, cramfs, ext4, ramdisk, ramfs/tmpfs,procfs等
      • ramdisk:将一部分固定大小的内存当作分区来使用。在编译内核时和内核一起打包,作为根文件系统
      • ramfs/tmpfs:基于内存的文件系统,大小随内容而变
      • procfs:启动时动态生成的伪文件系统,用于查看和设定内核参数
  • MTD(Memory Technology Device):存储技术设备,为底层硬件(Flash)和上层(文件系统)之间提供一个统一的抽象接口。它专门针对各种非易失性存储器(以Flash为主)而设计的,有基于扇区的擦除、读/写操作接口。

image-20210721143506133

Linux系统活动空间

  • 内核空间:处于系统态,拥有受保护的内存空间和访问硬件设备的所有权限。这种系统态和被保护起来的内存空间统称为内核空间。
  • 用户空间:与内核空间相对,应用程序在用户空间执行,只能看到允许他们使用的部分系统资源

区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性

32位系统中系统寻址空间:

image-20210723101328318

  • 高 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用
  • 低 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由用户进程使用
  • 需要硬件系统提供页机制(MMU)管理内存,实现内存映射、内存保护

处理器在任何时刻的活动必然为以下三种之一:

  1. 运行于用户空间,执行用户进程
  2. 运行于内核空间,处于进程上下文,代表某个特定进程执行
  3. 运行于内核空间,处于中断上下文,处理特定中断

内核态&用户态:

  • 内核态:当内核运行的时候,系统以内核态进入内核空间执行
  • 用户态:执行一个用户程序时,系统以用户态进入用户空间执行

Linux进程

  • 进程是处于执行期的程序(目标码存放在某种存储介质上)以及相关资源(打开的文件句柄、挂起的信号量、内存地址空间、执行线程,以及数据段等)的总称。程序不等于进程
  • 每个进程拥有一个唯一的标识符:PID
  • 进程提供两种虚拟机制:虚拟处理器和虚拟内存
  • init进程:PID为1。所有进程的祖先。在内核启动的最后阶段,由内核线程加载外部程序/sbin/init后拥有了用户态空间,蜕变成为一个用户进程。负责解析执行脚本/etc/inittab,完成系统的启动。

进程创建:fork()函数

  • fork()系统调用从内核返回两次

    • 返回子进程的PID给父进程
    • 返回0给子进程
  • fork()通过clone()实现,但它并没有直接复制所有的资源,而是通过写时拷贝技术来推迟或避免页拷贝。实际开销仅仅是复制进程页表和创建唯一的进程描述符。

exec()函数:创建新的地址空间,载入新程序

  • fork后立即调用exec(),就不会存在页被写入的情况,因而不会激活写时拷贝

exit()函数:终止进程并将占用的资源释放掉,同时:

  • 通知父进程为自己“收尸”
  • 给自己的子进程(如果存在)找一个新的父进程:一个进程退出后,其子进程变成孤儿进程,并由其父进程收养。

进程退出执行后,被设置为僵死状态。直至父进程通过wait()这组函数来为其“收尸”:等待子进程退出、接收返回值、释放进程描述符)

僵尸进程,ps命令中显示Z状态的进程,意味着其父进程没有调用wait()对其进行“收尸”。原因通常有两个:

  • 父进程编码错误,收到子进程退出信号后没有调用wait()
  • 子进程在主线程中调用pthread_exit()退出主线程,这种情况下,只要线程组中还有其他线程运行,这个进程都不会退出。因而也就不会发退出信号给父进程
    • 父进程是init的进程也是如此

Linux存在进程树,所有的进程都是init进程的后代。

init进程作为后台进程(daemon),接收其子进程的退出消息,并为其“收尸”。 每个进程都有一个父进程(PPID)。拥有同一个父进程的所有进程被称为兄弟。 一个进程退出后,其子进程变成孤儿进程,并由其父进程收养。

Linux内核没有线程的概念。在内核看来,线程只是一个普通的进程(只是线程和其他一些进程共享某些资源,如文件描述符、地址空间等)。和进程一样,每个线程都拥有唯一隶属于自己的task_sruct。

线程的创建和普通进程的创建类似,也是通过clone()系统调用完成:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)

而fork()的实现是:

clone(SIGCHLD, 0);

Linux进程调度

Linux进程调度的基本单位是线程

进程可以分为两种类型:

  • I/O消耗型,Linux更倾向于优先调度I/O消耗型进程。

  • CPU消耗型

进程优先级:Linux采用了两种不同的优先级范围

  • nice值(-20 ~ 19): 值越大意味着优先级越低。(普通进程)
  • 实时优先级(0~99):值越大意味着优先级越高。(实时进程)

通过ps命令查看,NI列即为进程的nice值,PR列即为进程的实时优先级 实时进程的优先级高于普通进程。所以实时优先级和nice优先级处于互不相交的两个范畴

进程调度策略:

  • SCHED_NORMAL:普通调度策略。基于CFS算法根据nice值对普通级别的进程进行公平调度。(不再有时间片概念,而是根据vruntime变量挑选下个任务)
  • SCHED_FIFO:实时先入先出调度策略。一旦一个SCHED_FIFO级进程处于可执行状态,就会一直执行。知道它自己受阻塞或显式地释放CPU。只有更高级别的SCHED_FIFO或SCHED_RR级进程任何才能抢占。
  • SCHED_RR:实时轮流调度策略,相当于带时间片的SCHED_FIFO——当SCHED_RR任务耗尽它的时间片时,同一级别的其他实时进程被轮流调度。

进程调度相关API

  • 设置调度策略和实时优先级:sched_setscheduler()
  • 获取调度策略和实时优先级:sched_getscheduler()
  • 将指定进程的静态nice值增加一个给定的量。只有超级用户才能在调用它时使用负值:nice()
  • 将指定进程绑定CPU:sched_setaffinity()
  • 显式让出CPU时间:sched_yield()

系统调用(syscall)是用户进程和内核交互的接口。应用程序通过这些接口,可以访问硬件设备,申请资源,创建新进层,进行IPC通信等。实际上,提供这些接口主要是为了保证同稳定可靠。

编程实践中,我们一般不是直接使用系统调用接口,而是通过在用户空间封装的API。

image-20210723101943177

原理:用户空间的程序无法直接调用内核空间中的函数。为了实现系统调用,用户程序通过触发异常(软中断)来使系统陷入内核,由内核中的系统调用处理程序system_call来执行相应的系统调用

image-20210723101958697

Linux互斥机制

  • 内核空间

    • 原子变量&原子操作
    • 自旋锁:在SMP系统用于处理器之间的互斥,适合保护很短的临界区,并且不允许在临界区睡眠。申请自旋锁的时候,如果自旋锁被其他处理器占有,本处理器自旋等待
    • 信号量:本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。信号量为0时,进程进入睡眠状态,直至信号量值大于0,进程被唤醒
    • 互斥锁:禁止多个线程同时进入受保护的代码“临界区”
    • 原子变量和自旋锁可应用于任何上下文。信号量和互斥锁只能应用于进程上下文
    atomic_t v = ATOMIC_INIT(0);
    atomic_dec(&v); //原子变量自减1
    
    spinlock_t spin;
    
    struct semaphore sem;
    
    struct mutex lock;
    
  • 用户空间

    • Futex:Fast Userspace Mutex(快速用户空间互斥体)。是一种用户态和内核态混合的同步机制。当进程/线程尝试进入或退出临界区时,会检查futex变量(一个存在特定内存位置(可以是共享内存)的整型变量)。如果竞争没有发生,则只会改变futex变量的值,而无须做wait/wake系统调用,从而大大提高效率。

      // 在uaddr指向的这个锁变量上挂起等待(仅当*uaddr==val时)
      int futex_wait(int *uaddr, int val);
      // 唤醒n个在uaddr指向的锁变量上挂起等待的进程
      int futex_wake(int *uaddr, int n);
      
    • 编程时,一般不会直接使用futex这种基础的接口,而是使用基于futex封装的更高级别的锁抽象,如信号量和互斥锁

      • 信号量:sem_t sem;
      • 互斥锁:pthread_mutex_t mutex;

Linux安全机制

  • DAC机制

    • 自主访问控制(DAC,Discretionary Access Control),基础的传统的Linux权限管理的机制。
    • DAC模型中,参与的对象有3种:主体(subject)、客体(object)、规则(policy)
    • 文件客体的所有者(或者管理员)负责管理访问控制
    • DAC简单高效,但权限划分粒度过大
    • uid权限升级:依赖于SUID(set-user-id)机制。如果文件设置了SUID,那么它在执行的时候,会把进程的权限(euid)设置成文件属主的uid

    image-20210723103358878

  • Capability机制

    • Linux 将传统上与超级用户 root 关联的特权划分为不同的单元,称为 Capabilites。比如发送信号(kill)CAP_KILL,设置系统时间CAP_SYS_TIME
    • Capabilites 作为线程的属性存在,每个单元可以独立启用和禁用。
    • 在执行特权操作时,如果进程的有效身份不是 root,就去检查是否具有该特权操作所对应的 capabilites,并以此决定是否可以进行该特权操作
    • 对DAC机制不足的补充。对root特权进行细粒度的控制,实现按需授权,从而减小系统的安全攻击面。
  • MAC

    • 强制访问控制(Mandatory Access Control),SELinux(Security Enhanced Linux)使用的安全策略。在DAC的基础上,把行为、规则、判定结果进一步细分。所以它的权限管理粒度更细,但是开销也稍大
    • 管理员管理访问控制,管理员指定策略,用户不能改变它,任何主体不能改变
    • MAC可以定义所有的进程(称为主体subject)对系统的其他部分(文件、设备、socket、端口和其它进程等,称为客体object)进行操作的权限或许可
    • 安全上下文由4个部分组成:身份标识(user):角色(role):类型(type):级别(level) 主客体间是否可以进行读写,主要在于Type的类型是否匹配,对于主体,它的类型称为domain,域;对于客体,它的类型称为Type,类型

Linux Shell

shell是Linux内核的外壳。提供了用户与内核交互操作的接口,接收用户输入的命令并把它送入内核去执行。

shell有自己的编程语言,具有普通编程语言的很多特点,比如它也有循环结构(for while)和分支控制结构(switch if)等。

GNU系统默认的shell是bash