普通Linux内核和ARM-Linux内核都是Linux系统,但是由于ARM和X86是不同的CPU架构,他们的指令集不同,所以软件编译环境不同,软件代码一般不能互用,一般需要进行兼容性移植。
源代码的arch目录中包含了和硬件结构相关的代码,每个平台都占有一个相应的目录。如下图:
Linux的内核版本编号格式一般为:
主版本号.次版本号.修订版本号
主版本号和次版本号标志着相较上一版本有重要的功能修改,且次版本号为偶数则代表该内核版本为稳定版本,为奇数则是非稳定版本或测试版本,修订版本号代表该内核版本修订次数。以Linux4.14.78版本为例,主版本号为4,次版本号为14,即稳定版本,78为修订次数。
(1)ARM-Linux代码结构
如果你已经获得了内核源代码,解压后你会看到一个较为清晰的源代码目录。此处仍然以Linux4.14.78版本为例,其源代码包含如下目录:
● arch:包含和处理器体系结构相关的代码,每种平台占一个相应的目录,如i386、ARM、ARM64、POWERPC、MIPS等。
● 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:进程间通信的代码。
Linux内核5个组成部分之间的依赖关系如下。
●进程调度与内存管理之间的关系:这两个子系统互相依赖。在多程序环境下,创建进程第一件事情就是将程序和数据装入内存。而进程调度过程也包含内存分配与重定位相关问题。
●进程间通信与内存管理的关系:进程间通信子系统要依赖内存管理支持共享内存通信机制,这种机制允许两个进程除了拥有自己的私有空间之外,还可以存取共同的内存区域。
●虚拟文件系统与网络接口之间的关系:虚拟文件系统利用网络接口支持网络文件系统(NFS),也利用内存管理支持RAMDISK设备。
●内存管理与虚拟文件系统之间的关系:内存管理利用虚拟文件系统支持交换,进程交换由调度程序定期调度,这也是内存管理依赖于进程调度的原因。
(2)ARM-Linux内核模块功能介绍
1)进程调度
进程调度控制系统中的多个进程对CPU的访问,使得多个进程能在CPU 中“微观串行,宏观并行”地执行。
2)内存管理
内存管理的主要作用是控制多个进程安全地共享主内存区域。当CPU提供内存管理单元(MMU)时,Linux内存管理对于每个进程就是完成从虚拟内存到物理内存的转换。
3)虚拟文件系统
如图所示,Linux系统所有设备访问都是以文件形式,同时还支持多种文件系统。Linux虚拟文件系统隐藏了各种硬件和文件系统的具体细节,为所有设备提供了统一的接口。
4)网络接口
网络接口提供了对各种网络标准的存取和各种网络硬件的支持。如图所示,在Linux中网络接口可分为网络协议和网络驱动程序。
Linux内核支持的协议栈种类较多,如Internet、UNIX、CAN、NFC、Bluetooth、WiMAX、IrDA等,上层的应用程序统一使用套接字接口。
5)进程间通信
应用程序在运行起来之后(进程),是相互独立的,都有自己的进程地址空间。但是往往在一些业务上需要进程间的通信,来完成系统的某个完整的功能。
Linux支持进程间的多种通信机制,包含信号量、共享内存、消息队列、管道、套接字等,这些机制可协助多个进程、多资源的互斥访问、进程间的同步和消息传递。
2、ARM-Linux进程管理与调度
1)进程的表示
进程就是处于执行期的程序及其所包含的资源总称,但进程并不仅仅局限于一段可执行程序代码。通常进程还要包含其他资源,像打开的文件、挂起的信号、内核内部数据、处理器状态等,以及一个或多个具有内存映射的内存地址空间及一个或多个执行线程,当然还包括用来存放全局变量的数据段等。
程序本身并不是进程,进程是一个过程的描述,是处于执行期的程序以及相关的资源的总称。
Linux内核涉及进程和程序都围绕一个名为task_struct的数据结构(有的操作系统叫任务控制块TCB)建立,该结构体中包含描述该进程内存资源、文件系统资源、文件资源、tty资源、信号处理等的指针。
线程:线程(thread)则是某一进程中一路单独运行的程序。也就是说,线程存在于进程之中。一个进程由一个或多个线程构成,各线程共享相同的进程代码和全局数据, 但线程有自己的线程代码、堆栈和局部变量。一个程序至少有一个进程,一个进程至少有一个线程。
内核进程:而内核进程是在内核空间的运行的进程,它可以被调度,也可以被抢占,但是没有独立的地址空间,只在内核空间运行,负责完成内核在后台执行的操作任务,只能由其他内核进程创建。
进程用户空间:
只读段:包含程序代码(.init 和.text)和只读数据(.rodata)。
数据段:存放的是全局变量和静态变量。其中可读可写数据段(.data)存放已初始化的全局变量和静态变量,BSS 数据段(.bss)存放未初始化的全局变量和静态变量。
栈:由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。
堆:存放动态分配的数据,一般由程序员动态分配和释放。若程序员不释放,程序结束时可能由操作系统回收。如图:
在Linux中,无论进程还是线程,到了内核里面,统一都叫作任务(Task)。Linux内核涉及进程和程序的所有算法都围绕一个名为task_struct的数据结构进行的。该结构体中包含描述该进程内存资源、文件系统资源、文件资源、tty资源、信号处理等的指针,将进程与各个内核子系统联系起来。
(1)任务ID
任务号用于操作系统进行排期、下发任务等。在内核中,虽然进程和线程都是任务,但是还是应该加以区分,因为任务下发和展示是区分进程级和线程级的,所以task_struct中有两个任务号,pid是processID,tgid是threadgroupID。
(2)Linux任务状态
操作系统在对任务进行调度时,任务会在不同的状态之间进行转换以满足任务调度管理的需要。在Linux操作系统中,任务存在运行、可中断睡眠、不可中断睡眠、暂停、僵死和就绪等几种状态。
运行状态(TASK_RUNNING):
当进程正在被CPU执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态(running)。
可中断睡眠状态(TASK_INTERRUPTIBLE):
当进程处于可中断等待状态时,系统不会调度该进程执行。当系统产生一个中断或者释放了进程正在等待的资源,或者进程收到一个信号,都可以唤醒进程转换到就绪状态\运行状态。
不可中断睡眠状态(TASK_UNINTERRUPTIBLE):
与TASK_INTERRUPTIBLE状态类似,但是此刻进程是不可中断的,进程不响应异步信号,即不能通过中断或者其他事件来唤醒,只有被使用wake_up()函数明确唤醒时才能转换到可运行的就绪状态。
暂停状态(TASK_STOPPED):
当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。
僵死状态(TASK_ZOMBIE):
进程在退出的过程中,处于TASK_DEAD状态。在这个退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸。
(3)进程信号处理。
该部分定义进程所用的信号处理程序,用于响应到来的信号。信号分为三种:阻塞暂不处理(blocked)、尚等待处理(pending)、信号处理函数进行处理(sighand)。处理的结果可以是忽略,也可以是结束任务。
(4)进程的内存管理。
每个进程都有自己独立的虚拟内存空间,这就需要一个数据结构来表示,这就是mm_struct。
(5)进程权限。
该部分用于控制进程能否访问某些文件、某些进程以及本进程能否被其它进程访问。
2)进程的生命周期与状态切换
进程并不总是可以立即运行。有时候它必须等待来自外部的信号或者不受其控制的事件,如果进程运行需要这些信号或者事件有效,在信号有效或者事件发生之前,进程无法运行。
当一个进程的运行时间片用完,进程会由运行态进入就绪等待状态,调度器将该进程插入就绪等待队列末尾以便于下一次调度。
如果进程在内核态执行时需要等待系统的某个资源(比如等待socket连接、信号量等),此时进程也会进入睡眠状态(TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE)。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。
当正在运行的进程挂起或者终止运行,进程会进入僵死状态;进程在退出的过程中,处于TASK_DEAD状态。在这个退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。
运行进程调用暂停系统服务就会进入暂停状态,暂停状态的进程可以通过系统调用SIGCONT来激活进入就绪态。
3)进程的创建和执行
内核在引导并完成了基本的初始化以后,就有了系统第一进程(实际上是内核线程)。除此之外,所有其他的进程和内核线程都有这个原始进程或其子孙进程所创建,都是这个原始进程的后代。
用户空间中通过vfork()和fork()函数进行子进程创建。
fork()和vfork()都是调用函数do_fork()来实现的。该函数调用copy_process()函数,然后让进程开始运行。如下图:
(1)fork系统调用
要创建一个进程,最基本的系统调用是fork()。调用fork时,系统将创建一个与当前进程相同的新进程。通常将原有的进程称为父进程,把新创建的进程称为子进程。子进程是父进程的一个拷贝,子进程获得同父进程相同的数据,但是也修改一些属性,例如数据空间、用户堆栈等。
(2)vfork系统调用
vfork系统调用和fork系统调用的功能基本相同。vfork系统调用创建的进程共享其父进程的内存地址空间,但是并不完全复制父进程的数据段,而是和父进程共享其数据段。为了防止父进程重写子进程需要的数据,父进程会被vfork调用阻塞,直到子进程退出或执行一个新的程序。由于调用vfork函数时父进程被挂起,所以如果我们使用vfork函数替换 forkdemo中的fork函数,那么执行程序时输出信息的顺序就不会变化了。
使用vfork创建的子进程一般会通过exec族函数执行新的程序。
(3)exec族函数
使用fork/vfork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往需要调用一个exec族函数以执行另外一个程序。当进程调用exec族函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的起始处开始执行。
4)进程的销毁
在Linux中,有两个函数exit()和_exit()函数都是用来终止进程的。当程序执行到exit()或_exit()时,进程会无条件地停止剩下的所有操作,清除各种数据结构,并终止本进程的运行。
3、Linux调度相关概念
进程调度的目的是使用一种策略,使得系统资源能够最大限度地发挥作用。
调度程序就是用来实现这一功能,它负责决定将哪个进程投入运行,何时运行以及运行多长时间。进程调度程序(简称调度程序)可看作在可运行态进程之间分配有限的处理器时间资源的内核子系统。
调度程序最大限度地利用处理器时间的原则是,只要有可以执行的进程,那么就总会有进程正在执行。
一般多任务系统可以划分为两类:非抢占式多任务(cooperative multitasking)和抢占式多任务(preemptive multitasking)。
Linux提供的是抢占式的多任务模式,在此模式下,由调度程序来决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会。
进程在被抢占之前可以运行的时间是预先设定好的时间片。有效管理时间片能使调度程序从系统全局角度做出调度决定,避免个别进程独占系统资源。
进程可以被分为I/O消耗型和处理器消耗型。
I/O消耗型的进程大部分时间用来提交I/O请求或是等待I/O请求,这样的进程经常处于可运行状态,但通常只运行很短时间,在等待I/O时会阻塞。这种进程一般优先级较高;
而处理器消耗型的进程大部分时间都在执行代码,主要用于数据处理,除非被抢占,否则一直执行,没太多I/O需求。这种进程一般优先级较低,容易被抢占。
实现进程调度的调度策略就是要在这两个矛盾中寻找平衡:进程响应迅速(响应时间短)和最大系统利用率(高吞吐量),Linux倾向于优先调度I/O消耗型。
4、Linux调度器策略:
Linux调度器是以模块方式提供的,这样做的目的是允许不同类型的进程可以有针对性地选择调度算法。
每个调度器都有一个优先级,它会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出,去选择下面要执行的那一个程序。
完全公平调度(CFS)是一个针对普通进程的调度类,在Linux中称为SCHED_NORMAL,CFS算法实现定义在文件kernelsched_fair.c中。在这种调度算法是基于时间片的调度,每个进程将能获得1/n的处理器时间——n是指可运行进程的数量。
Linux进程提供了两种优先级,一种是普通的进程优先级,第二个是实时优先级。前者适用SCHED_NORMAL调度策略,后者可选SCHED_FIFO或SCHED_RR调度策略。
任何时候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占,同级实时进程之间是按照FIFO(一次机会做完)或者RR(多次轮转)规则调度的。
SCHED_FIFO实现了一种简单的、先入先出的调度算法:它不使用时间片。处于可运行状态的SCHED_FIFO级的进程会比任何SCHED_NORMAL级的进程都先得到调度。一旦一个SCHED_FIFO级进程处于可执行状态,就会一直执行,直到它自己受阻塞或显式地释放处理器为止。
SCHED_RR与SCHED_FIFO大体相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再继续执行了。也就是说,SCHED_RR是带有时间片的SCHED_FIFO——这是一种实时轮流调度算法。当SCHED_RR任务耗尽它的时间片时,在同一优先级的其他实时进程被轮流调度。时间片只用来重新调度同一优先级的进程。
这两种实时算法实现的都是静态优先级。内核不为实时进程计算动态优先级。这能保证给定优先级别的实时进程总能抢占优先级比它低的进程。
实时进程优先级范围从0到MAX_RT_PRIO减1。默认情况下,MAX_RT_PRIO为100——所以默认的实时优先级范围是从0到99。
5、进程调度
如图所示,Linux 的进程在几个状态间进行切换。
6、ARM-Linux内存管理
由于内存管理涉及到物理内存和虚拟内存两方面,我们先来了解两者的基本含义。
物理内存和虚拟内存概念
由于内存管理涉及到物理内存和虚拟内存两方面,我们先来了解两者的基本概念。
在还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是十分有限的,这取决于CPU的地址线条数。比如在32位平台下,寻址的范围是2的32次方也就是4G。
使用物理地址访问内存存在以下问题:
1)进程地址空间不隔离,没有权限保护
由于程序都是直接访问物理内存,所以一个进程可以修改其他进程的内存数据,甚至修改内核地址空间中的数据。
2)内存使用效率低
当内存空间不足时,有多个进程要执行时,每个都要分配一定的内存空间,有限的物理内存不能满足需求,没有得到分配资源的进程就只能等待;只有当一个进程执行完以后,然后将新的程序装入内存运行,这种频繁的数据装入内存的操作效率低下,内存使用效率会十分低下。
3)程序运行的地址不确定
因为内存空间不足,需要执行的进程需要载入临时分配的内存,内存地址是随机分配的,所以程序运行的地址也是不确定的。
针对上面会出现的各种问题,虚拟内存就出现了。上述提到每一个进程运行时都会得到一定范围的虚拟内存。这个虚拟内存你可以认为,每个进程都认为自己拥有一段连续的内存空间,并且在需要时才在实际物理内存中进行数据交换。
实际上,在虚拟内存对应的物理内存上,是通过CPU中的MMU(MemoryManagementUnit-内存管理单元)来进行分配的,每个进程使用的虚拟内存地址是一个连续的地址空间,而实际上,MMU可能将该连续的虚拟地址空间映射到多个非连续物理内存空间。
ARM架构下的MMU:
内存管理需要系统内核和CPU架构的相互协作来完成,CPU架构下参与到内存管理的单元则是MMU。MMU它位于处理器内核和连接高速缓存以及物理存储器的总线之间。
MMU是处理器用来实现物理地址到虚拟地址映射的硬件单元。在ARM的体系结构中把 MMU作为协处理器来实现,即通过协处理器CPl5管理ARM的MMU。ARM 的MMU中除了实现虚拟物理地址的映射、访问权限外,还包括了对高速缓存和写缓冲的管理。
当ARM要访问存储器时,MMU先查找TLB (Translation Lookaside Buffer,旁路转换缓冲)中的虚拟地址表。如果TLB中没有虚拟地址的入口,则转换表遍历硬件会从存放在内存的转换表中获得转换和访问器权限。