文章目录
概述
linux内核简介
内核是硬件和软件之间的一个中间层。可以从三个角度看待内核:
- 应用程序的视角:内核将硬件系统抽象,隐藏底层细节,内核提供统一的应用运行环境。
- 进程线程的视角:内核充当资源管理程序,负责CPU时间、RAM、ROM等资源的调度分配。
- 编程者的视角:内核提供一组面向系统的命令或函数,用于向计算机发送请求。
微内核和宏内核
- 微内核:内核只负责最基本的功能,其它服务(内存管理、文件系统、设备驱动程序等)则由另外的程序实现。这种方式结构清晰、动态可扩展,但是系统所需的众多功能导致众多的服务进程。服务进程和内核需要复杂的通信,并且资源消耗大。
- 宏内核:是包括linux、win等大多数系统的选择,即将内核基本功能和所有服务程序都打包到内核中。但是这种方式没有微内核灵活。
注:linux和win随着技术的发展,在宏内核的基础上吸收了微内核的优点,称之为混合内核。
内核的组成介绍
- 应用程序、内核、硬件系统三者关系
- 内核的组成
内核源码目录
参考自《linux设备开发详解》
目录 | 说明 |
---|---|
arch | 包含和硬件体系结构相关的代码。每种平台占一个相应的目录,如i386、arm、arm64、powerpc、mips等。Linux内核目前已经支持30种左右的体系结构。在arch目录下,存放的是各个平台以及各个平台的芯片对Linux内核进程调度、内存管理、中断等的支持,以及每个具体的SoC和电路板的板级支持代码。 |
block | 块设备驱动程序I/O调度 |
crypto | 常用加密和散列算法(如AES、SHA等),还有一些压缩和CRC校验算法 |
documentation | 内核说明文档 |
drivers | 设备驱动程序,每个不同的驱动占用一个子目录,如char、block、net、mtd、i2c等 |
fs | 所支持的各种文件系统,如EXT、FAT、NTFS、JFFS2等 |
include | 头文件,与系统相关的头文件放置在include/linux子目录下 |
init | 内核初始化代码。著名的start_kernel()就位于init/main.c文件中 |
ipc | 进程间通信的代码 |
kernel | 内核最核心的部分,包括进程调度、定时器等,而和平台相关的一部分代码放在arch/*/kernel目录下 |
lib | 库文件代码 |
mm | 内存管理代码,和平台相关的一部分代码放在arch/*/mm目录下 |
net | 网络相关代码,实现各种常见的网络协议 |
scripts | 用于配置内核的脚本文件 |
security | 主要是一个SELinux的模块 |
sound | ALSA、OSS音频设备的驱动核心代码和常用设备驱动 |
usr | 实现用于打包和压缩的cpio等 |
文件系统
根文件目录
linux文件目录缩写:
路径名 | 全称 | 作用 |
---|---|---|
/bin | binaries | 可执行二进制文件。包含各种用户级gnu工具命令如cat,chmod(修改权限), chown, date, mv, mkdir, cp, bash等。 |
/boot | 启动目录,存放启动文件。 | |
/dev | devices | 设备目录。一个文件代表一个设备,将设备操作抽象为文件操作。 |
/etc | etcetera | 系统配置文件目录。主要放置配置文件,例如用户的帐号密码、各种服务的配置文件等。 常用的配置文件有:/etc/inittab, /etc/init.d/, /etc/modprobe.conf, /etc/X11/, /etc/fstab, /etc/sysconfig/等。 |
/home | 这是系统默认的用户主目录(home directory),可用存放用户文件。 | |
/lib | library | 库目录,存放系统和应用程序的库文件 |
/media | 媒体目录,可移动媒体设备的常用挂载点 | |
/mnt | mount | 挂载目录,另一个可移动媒体设备的常用挂载点 |
/proc | process | 这个目录本身是一个虚拟文件系统(virtual filesystem)。所有文件都是在内存当中,例如系统核心、进程(process)、设备及网络状态等。 |
/root | root用户的主目录 | |
/sbin | Superuser Binaries | 系统二进制目录,存放许多GNU管理员级工具 |
/run | 运行目录,存放系统运作时的运行时数据 | |
/sys | system | 系统目录,存放系统硬件信息的相关文件 |
/tmp | temporary | 临时目录,可以再该目录中创建和删除临时工作文件 |
/usr | Unix Shared Resources | 放置可分享的与不可变动的(shareable, static)的文件,主要是软件资源所放置的目录。 |
/var | variable | 可变目录,用以存放经常变化的文件,比如日志文件 |
虚拟文件系统
Linux虚拟文件系统隐藏了各种硬件的具体细节,为所有设备提供了统一的接口。而且,它独立于各个具体的文件系统,是对各种文件系统的一个抽象。它为上层的应用程序提供了统一的vfs_read()、vfs_write()等接口,并调用具体底层文件系统或者设备驱动中实现的file_operations结构体的成员函数。
内存管理
内存管理的主要作用是控制多个进程安全地共享主内存区域。当CPU提供内存管理单元(MMU)
时,Linux内存管理对于每个进程完成从虚拟内存到物理内存的转换。
进程与线程
基本概念
概述
- 进程是处于执行期的程序以及相关资源的总称,或者说是正在执行的程序代码的实时结果。一个进程包括可执行程序代码、资源(打开的文件、挂起的信号、内核数据、处理器状态)、内存地址空间及执行线程等。
- 线程是进程中活动的对象,每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器。在linux中线程是一种特殊的进程。线程之间共享虚拟内存,不共享虚拟处理器。内核调度的对象是线程而不是进程。
- 进程的四要素:
- 有一段程序供其执行(不一定是一个进程所专有的)
- 有自己的专用系统堆栈空间
- 有进程控制块(task_struct)
- 有独立的存储空间。共享用户空间的称为用户线程,没有用户空间称为内核线程。
进程状态
linux上进程有5种状态:
PS状态码 | 值 | 说明 |
---|---|---|
R | TASK_RUNNING | 可运行状态 。正在执行或着在run_queue队列里等待执行。 |
S | TASK_INTERRUPTIBLE | 可中断的睡眠状态。该进程被阻塞,当运行条件满足后内核将其设为R状态;当运行条件不满足但是接收到信号时,该进程也会进入R状态。 |
D | TASK_UNINTERRUPTIBLE | 不可中断的睡眠状态。该进程被阻塞,不接受和处理信号(包括SIGKILL信号)。只能当条件满足后由内核唤醒。 |
T | TASK_TRACED | 跟踪状态。例如通过ptrace对调试程序进行跟踪。 |
T | TASK_STOPPED | 停止状态。进程停止进行,没有投入运行也不能投入运行。当接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTOU信号,或者在调试状态接受到任何信号都会进入该状态。 |
Z | EXIT_ZOMIE | 僵死状态。等待父进程wait()信号。 |
Z | EXIT_DEAD | 退出状态. |
不同进程状态切换如下图所示:
- 设置进程状态
#include <linux/sched.h>
set_task_state(task,state);//将task的状态设置为state
set_current_state(state);//同上
//等价于
task->state=state;
进程上下文(process context)
进程上文:指进程由用户态切换到内核态是需要保存用户态时cpu寄存器中的值,进程状态以及堆栈上的内容,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
进程下文:指切换到内核态后执行的程序,即进程运行在内核空间的部分。
进程管理
进程描述符
- 描述符结构
内核把进程存放在一个双向循环链表中,链表节点的数据类型为task_struct。这种数据结构称为进程描述符,包含一个进程的所有信息,定义在inluce/sched.h
中。
task_struct的结构非常庞杂,其内容可以分解为以下几个部分:
- 状态和执行信息。例如暂停信号、二进制格式信息、pid、父进程和子进程的指针、优先级、CPU时间等。
- 在已分配的虚拟内存中的信息。
- 进程用户、组ID、权限等。
- 已使用文件信息。包括二进制文件和其它处理的文件系统信息。
- 线程信息。
- 进程间通信(IPC)信息。
- 信号处理程序。
- task_struct中重要的成员
state:指定了进程的当前状态。该变量可以取进程状态表中的状态值。 - 进程描述符的分配
- 使用slab分配进程描述符
linux通过slab分配器分配task_struct结构,实现对象复用和缓存着色(cache coloring),可以避免动态分配和释放所带来的资源消耗。用slab分配器动态生成task_struct结构会创建一个struct thread_info(定义在asm/thread_info.h中),thread_info放置在内存栈的尾部即栈顶(向上生长)或栈底(向下生长),其结构中第一项指向该进程的task_struct地址。
struct thread_info{
struct task_struct *task;//指向进程描述符
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count;
mm_segment addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
}
- 进程描述符的存放
内核通过进程标识符(PID)来标识每个进程,PID的最大值在/proc/sys/kernel/pid_max
定义,默认最大值为32768。处理进程一般通过task_struct进行,通过current宏查找当前运行进程的进程描述符,硬件体系不同该宏的实现也不同。当前进程task_struct存放在专门的寄存器中(硬件体系有)或thread_info结构中。
进程家族树
在linux中,进程是以树状结构生成的,第一个进程为init(进程号为1,wu)。每个task_struct都包含一个指向父进程(parent)和子进程(children)的子进程链表。因此可以通过以下方式获得其父进程或子进程:
struct task_struct *my_parent=current->parent;
struct task_struct *my_child =current->child;
上面两个程序只能以当前进程为中心获取其父进程或子进程,对于任意给定的进程,获取其父进程或子进程可以使用:
list_entry(task->task.next,struct task_struct,tasks);
list_entry(task->task.prev,struct task_struct,tasks);
遍历整个进程树的程序:
struct task_struct *task;
for_each_process(task){
printk("%s[%d]\n",task->comm,task->pid);//打印每一个进程的名称和pid
}
进程的创建与退出
- 进程创建
fork()通过拷贝当前进程创建一个子进程,父进程和子进程的区别仅仅在于PID、PPID和挂起信号。Linux fork()函数实现过程下图所示,在创建子进程时可以通过参数指定父、子进程需要共享的资源。
exec函数
使用fork()创建的子进程是父进程的复制,要想子进程执行不同的程序可以调用exec函数(包括)函数读取可执行文件并载入地址空间开始运行。fork()函数复制父进程资源后,如子进程调用exec执行不同的程序,则会将复制的资源释放掉重新加载新资源,这样会造成性能浪费。为了避免这种情况,linux使用的写时复制(COW)的策略,即fork时,父子进程共享资源,只有当任意一方需要修改资源时才将被修改的资源复制。
-
进程退出
进程调用exit()后会进入退出流程,主要工作由do_exit()完成,定义在
kernel/exit.c
。在do_exit()结束后,进程资源已经释放,进程进入死亡状态(Z)。系统仍保留系统描述符、task_struct等信息,等待父进程调用wait()获取后,系统会调用release_task()将进程描述符等信息删除。
线程
linux中线程是一种特殊的进程,每个线程都拥有task_struct,共享地址空间、虚拟内存等资源。
- 线程创建
在用户态中,使用pthread_create创建一个新的线程并开始运行,定义如下:
int pthread_create (pthread_t *__restrict __newthread, const pthread_attr_t *__restrict __attr, void *(*__start_routine) (void *), void *__restrict __arg)
创建进程fork()和创建线程pthread_create(),底层调用函数都为clone(),通过指定不同的参数即可创建进程或线程。
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0 );///<创建进程
clone(CLONE_SIGCHLD,0);///<创建线程
- 内核线程
内核线程只在内核空间运行,没有独立的地址空间。创建内核线程方法如下:
strcut task_struct *kthread_create(int (*thread_func) (void *data), void *data,const char namefnt[],...);
//其中thread_func为运行的进程函数,data为传递参数
新创建的内核线程处于不可运行状态,可以调用下面函数唤醒:
kthread_run(int (*thread_func) (void *data), void *data,const char namefnt[],...);
//参数与kthread_create相同
进程调度
进程调度用于控制多个进程对CPU的访问,使得多个进程能在CPU中“微观串行,宏观并行”地执行。进程调度处于系统的中心位置,内核中其他的子系统都依赖它,因为每个子系统都需要挂起或恢复进程。现有的操作系统大多都是抢占式多任务系统,在此模式下由调度程序根据调度策略决定进程的运行和等待。linux2.6以后的调度策略称为完全公平调度算法(CFS)。调度器部分涉及两个部分内容:调度策略和上下文切换。
调度策略
进程优先级调度
根据进程优先级进行调度是最基本的一类调度策略,根据进程的价值和CPU时间需求来对进程分级。优先级高的进程先运行,时间片也较长,低优先级的进程则相反。linux采用两种不同的优先级范围:
- nice值
范围从-20到+19,默认值为0。在linux中nice值代表时间片的比例,nice值越小代表优先级越高。使用ps -el
可查看系统进程,对应NI的即为nice值。
- 实时优先级
范围从0到99,数值越大表示优先级越高。任何实时进程的优先级都高于普通进程,可使用ps -eo state,uid,pid,ppid,rtprio,time,comm
查询进程对应的实时优先级。RTPRIO即为实时优先级项,其中-
表示为非实时进程。
CFS调度
实时调度
上下文切换
上下文切换即从一个进程切换到另一个进程,系统要完成虚拟内存切换和处理器状态切换。进程切换有两种方式:进程调用schedule()进行主动切换,内核通过设置need_resched标志来调用schedule()切换。进程切换更多是内核设置need_resched来进行的,常见场景是当某个进程被抢占时或优先级更高的进程可执行时内核主动切换进程。每个进程都包含一个need_resched标志,放置在thread_info结构体中。内核通过need_resched标志进行切换进程中的抢占情形可以分为用户抢占和内核抢占。
- 用户抢占
用户抢占是指从内核态返回用户态(系统调用或中断处理)时,如果当前进程的need_resched标志被设置则会进行进程调度。
-
内核抢占
内核抢占发生在以下四种场景:中断处理程序中、内核代码具备可抢占、内核调用schedule()、内核中的任务阻塞。
调度相关的系统调用
系统调用
简介
系统调用是用户空间进程和硬件设备之间的一个中间层,在linux系统中系统调用是用户空间访问内核的唯一手段,其主要有三个作用。1. 为用户空间提供硬件的抽象接口;2. 保证系统的稳定性和安全性;3.为多任务和虚拟内存提供保证。以printf()为例说明应用与系统调用的关系,如下图所示。
每个系统调用都拥有一个系统调用号,分配后不能更改,系统调用被删除时调用号也不会被回收。内核在sys_call_table
中记录了所有注册的系统调用,在x86-64中定义在arch/i386/kernel/syscall_64.c
。当用户态进程执行一个系统调用时,系统调用号被用来指明要执行的系统调用。
系统调用处理程序
用户空间的程序无法直接执行内核代码,而是以软中断的方式通知内核执行系统调用。软中断(中断号128)会引发一个异常,指定的异常处理程序则为系统调用处理程序system_call(),该处理程序会使系统切换到内核态称为陷入内核。陷入内核时需要同时传递系统调用号和其它必要参数(如read()需传递缓存地址),在x86中,使用eax寄存器传递调用号,使用ebx、ecx、edx等寄存器放置其它参数。read()系统调用过程下图所示。
信号
基本概念
信号是事件发生时对进程的通知机制,也称为软件中断。信号分为两大类:第一类是用于内核向进程通知事件的称为标准(传统)信号,第二类是实时信号。标准信号的编号为1 ~ 31,是不可靠信号(非实时的);编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。以下内容都默认为标准信号。
信号的产生、传递和处理
-
信号的产生:事件主要有以下三类:硬件异常、用户输入的信号、软件事件。
-
信号的传递:信号产生后会被传递给进程。如该进程正在运行或即将运行(该处“运行”指内核调度),信号则会被立即送达,其它情况则信号会处于等待状态。
-
信号的处理:信号到达后,根据信号的不同默认行为有以下6种操作:
操作 | 解释 |
---|---|
忽略信号(ignore) | 在内核阶段就将信号丢弃,信号不传递给进程,进程也不会知道有此信号 |
终止进程 (term) | 进程异常终止,如调用kill杀掉进程。注:调用exit()而产生的是正常终止 |
生成core dump file 并终止进程 (core) | core dump file包含进程虚拟内存的镜像,可用于分析。 |
停止进程(stop) | 暂停进程的执行。 |
恢复进程(resume) | 恢复执行之前被暂停的进程。 |
执行信号处理程序 | 由程序员自己定义 |
信号类型和默认行为
信号类型较多,这里部分给出。
信号 | 信号值 | 说明 | 默认行为 |
---|---|---|---|
SIGABRT | 6 | 当进程调用abort()函数时,内核向进程发送该信号。默认情况下会终止进程并产生core dump file。 | core |
SIGALRM | 14 | 通过调用alarm()或settimer()设置的定时器定时完成时,产生该信号。(注:可理解为定时器中断) | term |
SIGBUS | 7 | 该信号称为bus error,表明发生了某种内存访问错误。常见的一种memory access error 是使用mmap()创建内存映射,访问的地址超过映射文件结尾(即越界)。 | core |
SIGHLD或SIGCLD | 17 | 当子进程被终止(exit()退出或被信号终止)、停止、恢复时,内核会向父进程发送该信号。 | ignoore |
改变信号处置(行为)
unix系统提供两种API用于改变信号的默认行为,signal()和sigaction()。signal()是原始的API,相比于sigaction()有三个特点:接口简单、功能少、不同系统实现存在差异(可移植性差)。因此推荐使用sigaction()。
- signal()
#include <signal.h>
void ( *signal(int sig, void (*handler)(int)) ) (int);
//Returns previous signal disposition on success, or SIG_ERR on error
网络接口
网络接口提供了对各种网络标准的存取和各种网络硬件的支持。如图3.8所示,在Linux中网络接口可分为网络协议和网络驱动程序,网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备通信,每一种可能的硬件设备都有相应的设备驱动程序。