Linux发行版本 = 启动引导程序 + 内核 + 驱动 + 应用软件
Linux基本特性:
-
单内核(宏内核):作为一个单独的大过程来实现,所有内核服务运行在一个单独的大内核地址空间。相较微内核而言,具有简单和性能高的特点(直接函数调用,少了IPC的开销)
-
抢占式
-
支持多用户、多任务、多线程、多CPU(SMP)
-
模块化设计,可动态加载内核模块
-
万物皆文件
-
Unix基因:借鉴了Unix的许多设计并实现了Unix的API
Linux文件系统:
- 虚拟文件系统(VFS):对不同的文件系统做一个抽象,提供统一的API访问接口
- 嵌入式Linux应用中,主要的存储设备为RAM和FLASH
- 常用的基于存储设备的文件系统类型包括:jffs2, yaffs, cramfs, ext4, ramdisk, ramfs/tmpfs,procfs等
- ramdisk:将一部分固定大小的内存当作分区来使用。在编译内核时和内核一起打包,作为根文件系统
- ramfs/tmpfs:基于内存的文件系统,大小随内容而变
- procfs:启动时动态生成的伪文件系统,用于查看和设定内核参数
- MTD(Memory Technology Device):存储技术设备,为底层硬件(Flash)和上层(文件系统)之间提供一个统一的抽象接口。它专门针对各种非易失性存储器(以Flash为主)而设计的,有基于扇区的擦除、读/写操作接口。
Linux系统活动空间:
- 内核空间
- 用户空间
32位系统中:
- 高 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用
- 低 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由用户进程使用
- 需要硬件系统提供页机制(MMU)管理内存,实现内存映射、内存保护
处理器在任何时刻的活动必然为以下三种之一:
- 运行于用户空间,执行用户进程
- 运行于内核空间,处于进程上下文,代表某个特定进程执行
- 运行于内核空间,处于中断上下文,处理特定中断
内核态&用户态:
- 内核态:当内核运行的时候,系统以内核态进入内核空间执行
- 用户态:执行一个用户程序时,系统以用户态进入用户空间执行
Linux进程:
- 进程是处于执行期的程序(目标码存放在某种存储介质上)以及相关资源(打开的文件句柄、挂起的信号量、内存地址空间、执行线程,以及数据段等)的总称。
- 每个进程拥有一个唯一的标识符:PID
- 进程提供两种虚拟机制:虚拟处理器和虚拟内存
- init进程:PID为1。所有进程的祖先。在内核启动的最后阶段,由内核线程加载外部程序/sbin/init后拥有了用户态空间,蜕变成为一个用户进程。负责解析执行脚本/etc/inittab,完成系统的启动。
进程创建:fork()函数
- fork()通过clone()实现,但它并没有直接复制所有的资源,而是通过写时拷贝技术来推迟或避免页拷贝。实际开销仅仅是复制进程页表和创建唯一的进程描述符。
- fork()系统调用从内核返回两次:一次回到父进程,一次回到新产生的子进程
exec()函数:创建新的地址空间,载入新程序
exit()函数:终止进程并将占用的资源释放掉,同时:
- 通知父进程为自己“收尸”
- 给自己的子进程(如果存在)找一个新的父进程:一个进程退出后,其子进程变成孤儿进程,并由其父进程收养。
进程退出执行后,被设置为僵死状态。直至父进程通过wait()这组函数来为其“收尸”:等待子进程退出、接收返回值、释放进程描述符
僵尸进程,ps中显示Z状态的进程,意味着其父进程没有调用wait()对其进行“收尸”。原因通常有两个:
- 父进程编码错误,收到子进程退出信号后没有调用wait()
- 子进程在主线程中调用pthread_exit()退出主线程,这种情况下,只要线程组中还有其他线程运行,这个进程都不会退出。因而也就不会发退出信号给父进程
- 父进程是init的进程也是如此
Linux存在进程树,所有的进程都是init进程的后代。
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。
原理:用户空间的程序无法直接调用内核空间中的函数。为了实现系统调用,用户程序通过触发异常(软中断)来使系统陷入内核,由内核中的系统调用处理程序system_call来执行相应的系统调用
Linux互斥机制:
- 内核空间
- 原子变量&原子操作
- 自旋锁:在SMP系统用于处理器之间的互斥,适合保护很短的临界区,并且不允许在临界区睡眠。申请自旋锁的时候,如果自旋锁被其他处理器占有,本处理器自旋等待
- 信号量:本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。信号量为0时,进程进入睡眠状态,直至信号量值大于0,进程被唤醒
- 互斥锁:禁止多个线程同时进入受保护的代码“临界区”
- 原子变量和自旋锁可应用于任何上下文。信号量和互斥锁只能应用于进程上下文
- 用户空间
- Futex:Fast Userspace Mutex(快速用户空间互斥体)。是一种用户态和内核态混合的同步机制。当进程/线程尝试进入或退出临界区时,会检查futex变量(一个存在特定内存位置(可以是共享内存)的整型变量)。如果竞争没有发生,则只会改变futex变量的值,而无须做wait/wake系统调用,从而大大提高效率。
- 编程时,一般不会直接使用futex这种基础的接口,而是使用基于futex封装的更高级别的锁抽象,如信号量和互斥锁
- 信号量:sem_t sem;
- 互斥锁:pthread_mutex_t mutex;
Linux安全机制:
-
DAC机制
- 自主访问控制(DAC,Discretionary Access Control),基础的传统的Linux权限管理的机制。
- DAC模型中,参与的对象有3种:主体(subject)、客体(object)、规则(policy)
- 文件客体的所有者(或者管理员)负责管理访问控制
- DAC简单高效,但权限划分粒度过大
-
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,类型