首先来一张图说明GNU/Linux的基本的体系结构:
Linux 内核可以进一步划分成 3 层:最上面是系统调用接口,用户程序通过软件中断后,调用系统内核提供的功能,这个在用户空间和内核提供的服务之间的接口称为系统调用;系统调用接口之下是内核代码,可以更精确地定义为独立于体系结构的内核代码,这些代码是 Linux 所支持的所有处理器体系结构所通用的;内核代码之下是依赖于体系结构的代码,构成了通常称为 BSP(Board Support Package)的部分,这些代码用作给定体系结构的处理器和特定于平台的代码。
用户模式和内核模式
执行进程的模式有两种:用户模式和内核模式。你编写的代码和执行所链接的库运行在用户模式下。需要内核服务时执行的内核代码只能在内核模式下运行。这是为了安全性考虑,用户模式可以确保一个进程只能访问自己的内存空间,如果破坏了自己内部结构,它也只能影响到自己,而不涉及其它的进程,更不会影响到整个系统。在用户模式下,进程能访问的内存称为用户空间。内核需要维护数据结构以控制系统中每个进程,它提供了一个所有进程共享的内存区域,为了保证安全性,内核代码和数据结构必须严格独立于用户代码和数据,只有内存代码可以在内核模式下运行,访问的共享内核数据以及执行权限指令。把进程在内核模式下访问的内存区域叫做内核空间。内核空间只有一个,在内核模式下的进程才能访问,但是内核空间是每个进程所特有的。
在典型的32位系统下虚拟地址为3G/1G的分配方式。内核空间位于高地址0xC0000000-0xFFFFFFFF,而实际的物理地址对应的是0x00000000-0x3FFFFFFF
系统调用
进程通过系统调用进入和退出内核模式,许多POSIX函数是系统调用的简单封装,如open,close,ictol,write和read等。驱动设备只能在内核模式下执行,应用程序不能直接调用设备驱动程序,而是使用预定义的系统调用间接的访问驱动程序代码。
下面是系统调用read的一个例子:
#include <syscall.h>
….
n = syscall(SYS_read, fd, buffer, length)
Linux提供的系统调用列表由内核版本决定,并且不随时间变化而变化。然而,用于创建系统调用的机制却会因为不同处理器体系结构而不同。系统调用函数是一个用于创建系统调用的汇编代码的封装。命令strace对于追踪系统调用非常有用,因为利用它可以知道当前任务执行的情况。
通常用户代码先将参数压入堆栈或者保存在预定义的寄存器中,然后在使用中断调用系统调用处理器。中断处理器将进程转入到内核模式,并调用适当的系统调用。在内核模式下,它从寄存器中读取参数或使用特定的函数从用户空间复制参数。可移植程序不能直接使用系统调用,而是依赖库为其实现系统调用。系统调用随着操作系统版本变化而变化,库的调用则无需这些变化。
Linux使用syscall技术称为应用程序二进制接口(ABI),它并不是Linux所独有的。该技术可以用于其它操作系统,甚至是BOIS系统。它与API是不同的,API要求链接兼容的函数,而ABI步要求链接不需要运行的代码。这是可执行程序无需重建就可以在许多不同内核中运行的原因之一。一个运行在2.2内核下的静态链接的可执行程序可能在2.6内核下仍然可以运行,因为大多数常见的系统调用接口从不会变化。
用户空间和内核空间的数据传递
用户模式下的内核空间是不可见的,当访问用户空间时,内核模式也必须格外注意。因此,通过系统调用传送数据非常的麻烦。虽然简单的参数可以通过寄存器传递,但是大型的内存必须通过复制,导致效率相当的低下。一些系统调用(read/write)需要在用户模式和内核模式之间传递大量数据,虽然这些额外的复制作用不大,但是它能维持内核空间和用户空间的相对独立。
虽然复制在短期看来会降低程序的性能,但从长远来看它有助于性能的提高,例如文件系统高速缓存,当数据写入文件时,数据被写入磁盘之前会先写入到内核空间,所以write这个动作可以在后台完成,应用程序可以通过flush来刷新用户空间的缓冲,使得程序继续执行。
进程调度程序
解决多任务操作系统问题的算法叫做程序调度(Scheduler),Linux内核提供多种调度算法,并且允许用户在系统安装时选择合适的调度算法。
调度程序的调用通常嵌入到系统调用中,并在进程需要等待事件时发生。一个与设备进行大量通信的进程会经常调用调度程序,由于设备的速度很慢,进程的大部分运行时间都浪费在等待过程中,这样运行时不会耗费太多的CPU时间。
注:何为协同多任务处理(Cooperativer multitasking)。
一个进程若不进行I/O,那就会占用大量的CPU时间,从而使得其它进程无法使用CPU,此时需要采用抢占式任务处理(Preemptive multitasking)。
每个Linux进程都有一个执行时间片或quantum,当一个进程因为时间片使用完被内核停止执行时,这个进程被抢占,如果有一个享有更高优先级的进程在就绪队列中,内核同样可以抢占正在执行的进程。一个进程也可以自动放弃CPU,可以通过sched_yield系统调用来放弃CPU。也可以通过其它系统调用(sleep)来实现对CPU的放弃。当进程在内核模式下等待一个事件时,称为阻塞(blocking)。一个阻塞进程既不会占用CPU,也不会被调度程序使用。
所有抢占式多任务处理操作系统都实现了按优先级调度策略。高优先权的进程总是比低优先级的进程先被调度。虽然用户可以影响进程的优先级,但进程的优先级最终还是由内核决定。Linux内核通过使用动态优先级(Dynamic Priority),不断提高和降低正在运行进程的优先级,从而使得低优先级的进程也能运行。
Linux内核调度策略一个重要目的是保证所有进程都有机会被调度,也就是说每个任务都有相应的使用CPU的时间。进程的有效优先级就是静态优先级和额外值之和,这个额外值可正可负。
ps命令选项中-C显示进程与参数的配对项,如-C <proc_name>,-o用于控制输出的格式,如etime,pid,pri,cmd。
内核允许用户通过使用一个名为nice的数值来影响调度程序关于优先级的调度。正的nice可以降低优先级,负的nice可以提高优先级。请查看nice和renice命令。
实时优先权
响应时间是指软件响应外部事件的时间,比如中断。严格的响应时间的应用程序通常被称为实时应用程序。Linux中实时进程的优先级范围是41-139,实时优先级越大,优先级越高。实时优先级在整个生命周期中值是不变的。
在设计实时进程时,必须确定它的调度策略,POSIX为实时进程指定了两种策略:先进先出(FIFO)和时间片法(round robin)。
创建实时进程的方法就是使用chrt命令,chrt在内部调用fork和exec和POSIX函数来设置优先级,函数如下:
int sched_setscheduler(pid_t pid, int policy, const struct sched_paramp);
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_paramp);
int sched_get_priority_min/max(int policy);
第一个为进程服务,第二个为线程服务。
sodu chrt --fifo 50 ./chewer &
Linux内核源码基础、
对于Linux内核的单内核模式的系统,可以把它分为如下:
Linux内核又可以分为5个大的模块:主要由进程调度(SCHED)、内存管理(MM)、虚拟文件系统(VFS)、网络接口(NET)和进程间通信(IPC)五个子系统组成。
Linux中进程间通信机制包括:管道(有名管道和无名管道)、消息队列、信号、套接字、共享内存、信号量。Linux中线程间的通信机制主要有:锁机制(包括互斥锁、条件变量、读写锁)、信号量机制(Semaphore:包括无名线程信号量和命名线程信号量)、信号机制(Signal:类似进程间的信号处理)。线程间通信的作用主要是用于线程之间的同步,所以线程间没有像进程通信中的用于数据交换的机制
1. 进程调度
进程调度控制系统中的多个进程对CPU的访问,使得多个进程能在CPU中“微观串行,宏观并行”地执行。进程调度处于系统的中心位置,内核中其他的子系统都依赖它,因为每个子系统都需要挂起或恢复进程。
Linux的进程在几个状态间进行切换。在设备驱动编程中,当请求的资源不能得到满足时,驱动一般会调度其他进程执行,并使本进程进入睡眠状态,直到它请求的资源被释放,才会被唤醒而进入就绪态。睡眠分成可被打断的睡眠和不可被打断的睡眠,两者的区别在于可被打断的睡眠在收到信号的时候会醒。在设备驱动编程中,当请求的资源不能得到满足时,驱动一般会调度其他进程执行,其对应进程进入睡眠状态,直到它请求的资源被释放,才会被唤醒而进入就绪态。设备驱动中,如果需要几个并发执行的任务,可以启动内核线程,启动内核线程的函数为:pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);
当用户使用系统提供的库函数进行进程编程,用户可以动态地创建进程,进程之间还有等待,互斥等操作,这些操作都是由linux内核来实现的。linux内核通过进程管理子系统实现了进程有关的操作,在linux系统上,所有的计算工作都是通过进程表现的,进程可以是短期的(执行一个命令),也可以是长期的(一种网络服务)。linux系统是一种动态系统,通过进程管理能够适应不断变化的计算需求。
在用户空间,进程是由进程标示符(PID)表示的。从用户角度看,一个PID是一个数字值,可以唯一标识一个进程,一个PID值在进程的整个生命周期中不会更改,但是PID可以在进程销毁后被重新使用。创建进程可以使用几种方式,可以创建一个新的进程,也可以创建当前进程的子进程。
在linux内核空间,每个进程都有一个独立的数据结构,用来保存该进程的ID、优先级、地址的空间等信息,这个结构也被称做进程控制块(Process Control Block)。所谓的进程管理就是对进程控制块的管理。
linux的进程是通过fork()函数系统调用产生的。调用fork()的进程叫做父进程,生成的进程叫做子进程。子进程被创建的时候,除了进程ID外,其它数据结构与父进程完全一致。在fork()系统调用创建内存之后,子进程马上被加入内核的进程调试队列,然后使用exec()系统调用,把程序的代码加入到子进程的地址空间,之后子进程就开始执行自己的代码。
在一个系统上可以有多个进程,但是一般情况下只有一个CPU,在同一个时刻只能有一个进程在工作,即使有多个CPU,也不可能和进程的数量一样多。如果让若干的进程都能在CPU上工作,这就是进程管理子系统的工作。linux内核设计了存放进程队列的结构,在一个系统上会有若干队列,分别存放不同状态的进程。一个进程可以有若干状态,具体是由操作系统来定义的,但是至少包含运行态、就绪态和等待3种状态,内核设计了对应的队列存放对应状态的进程控制块。
当一个用户进程被加载后,会进入就绪态,被加入到就绪态队列,CPU时间被轮转到就绪态队列后,切换到进程的代码,进程被执行,当进程的时间片到了以后被换出。如果进程发生I/O操作也会被提前被换出,并且存放到等待队列,当I/O请求返回后,进程又被放入就绪队列。linux系统对进程队列的管理设计了若干不同的方法,主要的目的是提高进程调试的稳定性。
2. 内存管理
内存管理的主要作用是控制多个进程安全地共享主内存区域。当CPU提供内存管理单元(MMU)时,Linux内存管理完成为每个进程进行虚拟内存到物理内存的转换。Linux 2.6引入了对无MMU CPU的支持。
使用虚拟内存技术的计算机,内存管理的硬件按照分页方式管理内存。分页方式是把计算机系统的物理内存按照相同大小等分,每个内存分片称作内存页,通常内存页大小是4KB。Linux内核的内存管理子系统管理虚拟内存与物理内存之间的映射关系,以及系统可用内存空间。内存管理要管理的不仅是4KB缓冲区。Linux提供了对4KB缓冲区的抽象,例如slab分配器。这种内存管理模式使用4KB缓冲区为基数,然后从中分配结构,并跟踪内存页使用情况,比如哪些内存页是满的,哪些页面没有完全使用,哪些页面为空。这样就允许该模式根据系统需要来动态调整内存使用。
在支持多用户的系统上,由于内存占用的增大,容易出现物理内存被消耗尽的情况。为了解决物理内存被耗尽的问题,内存管理子系统规定页面可以移出内存并放入磁盘中,这个过程称为交换。内存管理的源代码可以在./linux/mm中找到。
3. 虚拟文件系统
Linux虚拟文件系统(VFS)隐藏各种了硬件的具体细节,为所有的设备提供了统一的接口。而且,它独立于各个具体的文件系统,是对各种文件系统的一个抽象,它使用超级块super block存放文件系统相关信息,使用索引节点inode存放文件的物理信息,使用目录项dentry存放文件的逻辑信息。
在不同格式的文件分区上,程序都可以正确地读写文件,并且结果是一样的。有时在使用linux系统的时候发现,可以在不同类型的文件分区内直接复制文件,对应用程序来说,并不知道文件系统的类型,甚至不知道文件的类型,这就是虚拟文件系统在背后做的工作。虚拟文件系统屏蔽了不同文件系统间的差异,向用户提供了统一的接口。
虚拟文件系统,即VFS(Virtual File System)是Linux内核中的一个软件抽象层。它通过一些数据结构及其方法向实际的文件系统如ext2,vfat等提供接口机制。通过使用同一套文件 I/O 系统调用即可对Linux中的任意文件进行操作而无需考虑其所在的具体文件系统格式;更进一步,文件操作可以在不同文件系统之间进行。在linux系统中,一切都可以被看做是文件。不仅普通的文本文件、目录可以当做文件进行处理,而且字符设备、块设备、套接字等都可以被当做文件进行处理。这些文件虽然类型不同,但是却使用同一种操作方法。这也是UNIX/Linux设计的基本哲学之一。
虚拟文件系统(简称VFS)是实现“一切都是文件”特性的关键,是Linux内核的一个软件层,向用户空间的程序提供文件系统接口;同时提供了内核中的一个抽象功能,允许不同类型的文件系统存在。VFS可以被理解为一种抽象的接口标准,系统中所有的文件系统不仅依靠VFS共存,也依靠VFS协同工作。为了能够支持不同的文件系统,VFS定义了所有文件系统都支持的、最基本的一个概念上的接口和数据结构,在实现一个具体的文件系统的时候,需要向VFS提供符合VFS标准的接口和数据结构,不同的文件系统可能在实体概念上有差别,但是使用VFS接口时需要和VFS定义的概念保持一致,只有这样,才能实现对用户的文件系统无关性。VFS隐藏了具体文件系统的操作细节,所以,在VFS这一层以及内核其他部分看来,所有的文件系统都是相同的。对文件系统访问的系统调用通过VFS软件层处理,VFS根据访问的请求调用不同的文件系统驱动的函数处理用户的请求。文件系统的代码在访问物理设备的时候,需要使用物理设备驱动访问真正的硬件。
4. 网络接口
网络接口提供了对各种网络标准的存取和各种网络硬件的支持。如下图5所示,在Linux中网络接口可分为网络协议和网络驱动程序,网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备通信,每一种可能的硬件设备都有相应的设备驱动程序。
写网络应用程序,使用socket通过TCP/IP协议与其他机器通信,和前面介绍的内核子系统相似,socket相关的函数也是通过内核的子系统完成的,担当这部分任务的是内核的网络子系统,有时也把这部分代码称为“网络堆栈”。Linux内核提供了优秀的网络处理能力和功能,这与网络堆栈代码的设计思想是分不开的,Linux的网络堆栈部分沿袭了传统的层次结构,网络数据从用户进程到达实际的网络设备需要四个层次:用户进程,套接字,网络协议,网络设备。
实际上,在每层里面还可以分为好多层次,数据传输的路径是按照层次来的,不能跨越某个层次。linux网络子系统对网络层次采用了类似面向对象的设计思路,把需要处理的层次抽象为不同的实体,并且定义了实体之间的关系和数据处理流程:
- 网络协议:网络协议可以理解为一种语言,用于网络中不同设备之间的通信,是一种通信的规范。
- 套接字:套接字是内核与用户程序的接口,一个套接字对应一个数据连接,并且向用户提供了文件I/O,用户可以像操作文件一样在数据连接上收发数据,具体的协议处理由网络协议部分处理。套接字是用户使用网络的接口。
- 设备接口:设备接口是网络子系统中软件和硬件的接口,用户的数据最终是需要通过网络硬件设备发送和接收的,网络设备千差万别,设备驱动也不尽相同,通过设备接口屏蔽了具体设备驱动的差异。
- 网络缓冲区:网络缓冲区也称为套接字缓冲区(sk_buff),是网络子系统中的一个重要结构。网络传输数据存在许多不定因素,除了物理设备对传输数据的限制(例如MMU),网络受到干扰、丢包、重传等,都会造成数据的不稳定,网络缓冲区通过对网络数据的重新整理,使业务处理的数据包是完整的。网络缓冲区是内存中的一块缓冲区,是网络系统与内存管理的接口。
5. 进程间通信
进程通信支持提供进程之间的通信,Linux支持进程间的多种通信机制,包含信号量、共享内存、管道等,这些机制可协助多个进程、多资源的互斥访问、进程间的同步和消息传递。
子系统之间的依赖关系:
Linux内核的5个组成部分之间的依赖关系如下:
- 进程调度与内存管理之间的关系:这两个子系统互相依赖。在多道程序环境下,程序要运行必须为之创建进程,而创建进程的第一件事情,就是将程序和数据装入内存。
- 进程间通信与内存管理的关系:进程间通信子系统要依赖内存管理支持共享内存通信机制,这种机制允许两个进程除了拥有自己的私有空间,还可以存取共同的内存区域。
- 虚拟文件系统与网络接口之间的关系:虚拟文件系统利用网络接口支持网络文件系统(NFS),也利用内存管理支持RAMDISK设备。
- 内存管理与虚拟文件系统之间的关系:内存管理利用虚拟文件系统支持交换,交换进程(swapd)定期由调度程序调度,这也是内存管理依赖于进程调度的惟一原因。当一个进程存取的内存映射被换出时,内存管理向文件系统发出请求,同时,挂起当前正在运行的进程。
除了这些依赖关系外,内核中的所有子系统还要依赖于一些共同的资源。这些资源包括所有子系统都用到的例程,如分配和释放内存空间的函数、打印警告或错误信息的函数及系统提供的调试例程等。
Linux中的物理内存可以分为以下的部分:内核占用内存开始部分;接下来是共硬盘,软盘使用的高速缓冲区部分,其中扣除显存和bios的640k到1m;然后是虚拟盘;最后一部分是为所有程序可以使用的主内存区。
关于Linux进程
进程可以在内核态或者用户态运行,当资源可用就被唤醒,进入就绪态;当进程处于可中断睡眠状态,收到信号可被唤醒;当处于不可中断睡眠状态,只能被使用wakeup等的唤醒;当进程处于暂停状态,可发送信号使其进入就绪态;当僵死状态,当已经停止运行,父进程还没有调用wait查询状态,一旦父进程调用完wait取得子进程信息后,这个进程任务数据结构就会被释放掉。