视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
嵌入式Linux内核总结
2025-10-02 19:13:11 责编:小OO
文档
嵌入式Linux的内核

Linux是最受欢迎的自由电脑操作系统内核。它是一个用C语言写成,符合POSIX标准的类Unix操作系统。Linux最早是由芬兰黑客 Linus Torvalds为尝试在英特尔x86架构上提供自由免费的类Unix操作系统而开发的。该计划开始于1991年,在计划的早期有一些Minix 黑客提供了协助,而今天全球无数程序员正在为该计划无偿提供帮助。 技术上说Linux是一个内核。“内核”指的是一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件。一个内核不是一套完整的操作系统。一套基于Linux内核的完整操作系统叫作Linux操作系统,或是GNU/Linux。

1.嵌入式Linux内核的概述    

一、内核的概念

操作系统是一个用来和硬件打交道并为用户程序提供一个有限服务集的低级支撑软件。一个计算机系统是一个硬件和软件的共生体,它们互相依赖,不可分割。计算机的硬件,含有外围设备、处理器、内存、硬盘和其他的电子设备组成计算机的发动机。但是没有软件来操作和控制它,自身是不能工作的。完成这个控制工作的软件就称为操作系统,在Linux的术语中被称为“内核”,也可以称为“核心”。Linux内核的主要模块(或组件)分以下几个部分:存储管理、CPU和进程管理、文件系统、设备管理和驱动、网络通信,以及系统的初始化(引导)、系统调用等。

二、内核版本号

一般地,可以从Linux内核版本号来区分系统是否是Linux稳定版还是测试版。以版本2.4.0为例,2代表主版本号,4代表次版本号,0代表改动较小的末版本号。在版本号中,序号的第二位为偶数的版本表明这是一个可以使用的稳定版本,如2.2.5,而序号的第二位为奇数的版本一般有一些新的东西加入,是个不一定很稳定的测试版本,如2.3.1。这样稳定版本来源于上一个测试版升级版本号,而一个稳定版本发展到完全成熟后就不再发展。

三、内核发展历史

Linux最早是由芬兰人Linus Torvalds设计的。当时由于UNIX的 

商业化,Andrew Tannebaum教授开发了Minix操作系统以便于不受AT&T许可协议的约束,为教学科研提供一个操作系统。当时发布在Internet上,免费给全世界的学生使用。Minix具有较多UNIX的特点,但与UNIX不完全兼容。1991年10月5日,Linus为了给Minix用户设计一个比较有效的UNIX PC版本,自己动手写了一个“类Minix”的操作系统。整个故事从两个在端终上打印AAAA...和BBBB...的进程开始的,当时最初的内核版本是0.02。Linus Torvalds将它发到了Minix新闻组,很快就得到了反应。Linus Torvalds在这种简单的任务切换机制上进行扩展,并在很多热心支持者的帮助下开发和推出了Linux的第一个稳定的工作版本。1991年11月,Linux0.10版本推出,0.11版本随后在1991年12月推出,当时将它发布在Internet上,免费供人们使用。当Linux非常接近于一种可靠的/稳定的系统时,Linus决定将0.13版本称为0.95版本。1994年3月,正式的Linux 1.0出现了,这差不多是一种正式的宣言。截至那时为止,它的用户基数已经发展得很大,而且Linux的核心开发队伍也建立起来了。

2.嵌入式Linux内核的映像结构

实时系统指系统的计算正确性不仅取决于计算的逻辑正确性,还取决于产生结果的时间。如果未满足系统的时间约束,则认为系统失效。换句话说,系统面对变化的负载(从最小到最坏的情况)时必须确定性地保证满足时间要求。

  注意,实时性与速度关系不大:它与可预见性有关。例如,使用快速的现代处理器时,Linux 可以提供 20 μ 微秒的典型中断响应,但有时候响应会变得很长。这是一个基本的问题:并不是 Linux 不够快或效率不够高,而是因为它不能提供确定性。

  当中断到达时(event),CPU 发生中断并转入中断处理。执行一些工作以确定发生了什么事件,然后执行少量工作分配必需的任务以处理此事件(上下文切换)。中断到达与分发必需任务之间的时间(假设分配的是优先级最高的任务)称为响应时间。对于实时性要求,响应时间应是确定的并应当在已知的最坏情况的时间内完成。因此,对于某些高安全性的场合,操作系统应快速地分配任务,并且不允许其他非实时处理进行干扰。晚一秒钟响应比没有响应的情况更糟糕。

  除为中断处理提供确定性外,实时处理也需要支持周期性间隔的任务调度。大量控制系统要求周期性采样与处理。某个特定任务必须按照固定的周期(p)执行,从而确保系统的稳定性。在某些控制场合下,为了保持控制系统的正常工作,传感器的采样与控制必须按照一定的周期间隔。这意味着必须抢占其他处理,以便特定任务能按照期望的周期执行。

  能够在指定的期限完成实时任务(即便在最坏的处理负载下也能如此)的操作系统称为硬实时 系统。但并不是任何情况下都需要硬实时支持。如果操作系统在平均情况下能支持任务的执行期限,则称它为软实时 系统。硬实时系统指超过截止期限后将造成灾难性后果(例如展开气囊过晚或制动压力产生的滑行距离过长)的系统。软实时系统超过截止期限后并不会造成系统整体失败(如丢失视频中的一帧)。

  Linux 架构支持通过以下几种方式实现硬实时。

  1. 瘦内核方法

  瘦内核(或微内核)方法使用了第二个内核作为硬件与 Linux 内核间的抽象接口。非实时 Linux 内核在后台运行,作为瘦内核的一项低优先级任务托管全部非实时任务。实时任务直接在瘦内核上运行。

  瘦内核主要用于(除了托管实时任务外)中断管理。瘦内核截取中断以确保非实时内核无法抢占瘦内核的运行。这允许瘦内核提供硬实时支持。

  虽然瘦内核方法有自己的优势(硬实时支持与标准 Linux 内核共存),但这种方法也有缺点。实时任务和非实时任务是的,这造成了调试困难。而且,非实时任务并未得到 Linux 平台的完全支持(瘦内核执行称为瘦 的一个原因)。

  使用这种方法的例子有 RTLinux。

  2. 超微内核方法

  瘦内核方法依赖于包含任务管理的最小内核,而超微内核法对内核进行更进一步的缩减。通过这种方式,它不像是一个内核而更像是一个硬件抽象层(HAL)。超微内核为运行于更高级别的多个操作系统提供了硬件资源共享。

  这种方法和运行多个操作系统的虚拟化方法有一些相似之处。使用这种方法的情况下,超微内核在实时和非实时内核中对硬件进行抽象。这与 hypervisor 从客户(guest)操作系统对裸机进行抽象的方式很相似。

  3. 资源内核法

  这种方法为内核增加一个模块,为各种资源提供预留(reservation)。这种机制保证了对时分复用(time-multiplexed)系统资源的访问(CPU、网络或磁盘带宽)。这些资源拥有多个预留参数,如循环周期、需要的处理时间(也就是完成处理所需的时间),以及截止时间。

  资源内核提供了一组应用程序编程接口(API),允许任务请求这些预留资源。然后资源内核可以合并这些请求,使用任务定义的约束定义一个调度,从而提供确定的访问(如果无法提供确定性则返回错误)。通过调度算法,内核可以处理动态的调度负载。

  资源内核法实现的一个示例是 CMU 公司的 Linux/RK,它把可移植的资源内核集成到 Linux 中作为一个可加载模块。这种实现演化成商用的 TimeSys Linux/RT 产品。

3.嵌入式Linux内核的移植流程概述

内核作为Linux操作系统的核心,管理着系统线程、进程、资源和资源分配。根据需要应对内核进行重新裁剪,增加或消除对某些特定设备或子系统。

移植过程

1、bootloader启动内核:

vivi执行boot_kernel()->call_linux()来启动linux内核;在boot_kernel中有一部分(copy_kernel_img()函数)是内核映射拷贝过程,最后是pc跳到内核在ram中的地址 0x30008000开始执行内核程序:

#define DRAM_BASE0 0x30000000 /* base address of dram bank 0 */

#define DRAM_BASE DRAM_BASE0

#define LINUX_KERNEL_OFFSET 0x8000

to = boot_mem_base + LINUX_KERNEL_OFFSET;/*0x30008000,内核在内存中的位置*/

call_linux(0, mach_type, to);

Copy linux kernel from 0x00030000 to 0x30008000, size = 0x00200000 ... done

/*上面是vivi启动内核时的打印信息,0x00030000是内核在flash中的地址,这里把内核从flash中拷贝到ram中运行,大小为2M。*/下面是内核启动参数的拷贝过程,也是在boot_kernel()函数中实现的:

setup_linux_param(boot_mem_base + LINUX_PARAM_OFFSET);这里传递给内核的参数的首地址是0x30000100。

boot_mem_base: 0x30000000。

LINUX_PARAM_OFFSET:0x100

而我们知道vivi启动的时候首先拷贝自己到ram中,首地址是 0x33f00000,在链接脚本文件可以看到,对于M的flash而言(0x30000000~ 0x34000000)。

2、内核运行流程:

编译完成内核后,会生成vmlinux Image zImage三个文件,它们的主要区别是:

vmlinux: 是一个elf格式的文件

Image: 是经过objcopy 处理了的只包含内核代码,数据的一个文件, 已经不是elf格式的了。此时还没有经过压缩

arch/arm/boot/compressed/vmlinux: 是经过压缩的Image和加入了解压头的elf格式的文件

arch/arm/boot/zImage: 是经过objcopy处理了的可以直接下到对应的地址执行的内核镜像

通常我们把zImage下载在bootloader指定的flash分区即可。在s2410平台上,该入口是物理地址0x30008000。如果我们使用zImage自解压内核映像,对应的代码正是自解压头,位置在内核源码linux-的arch/arm/boot/compressed/head.S第 114行的start符号。

(1).在顶层MAKEFILE中,包含include $(srctree)/arch/$(ARCH)/Makefile

(此Makefile中head-y=arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o)

vmlinux-init := $(head-y) $(init-y)

vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)

vmlinux-all := $(vmlinux-init) $(vmlinux-main)

vmlinux-lds := arch/$(ARCH)/kernel/vmlinux.lds

从以上,可以看出,内核首先执行的是head.S

(2)第一阶段:arch/arm/boot/compressed/head.S

从head.S到init/main.c中的start_kernel执行过程:

./arch/arm/kernel/head-common.S:55: b start_kernel

(3)第二阶段: start_kernel

这里的函数每一个都是重量级的函数,有空可以好好分析分析。

细节:

1、##:表示字符串的连接。

2、__init,__initdata:这个定义在kernel的init段中,见arch/$(ARCH)/kernel/vmlinux.lds中。

. = (0xc0000000) + 0x00008000;

.init : { /* Init code and data */

_stext = .;

_sinittext = .;

*(.init.text)

_einittext = .;

__proc_info_begin = .;

*(.proc.info.init)

__proc_info_end = .;

__arch_info_begin = .;

*(.arch.info.init)

__arch_info_end = .;

__tagtable_begin = .;

*(.taglist.init)

__tagtable_end = .;

. = ALIGN(16);

__setup_start = .;

*(.init.setup)

__setup_end = .;

__early_begin = .;

*(.early_param.init)

__early_end = .;

__initcall_start = .;

*(.initcall1.init)

*(.initcall2.init)

*(.initcall3.init)

*(.initcall4.init)

*(.initcall5.init)

*(.initcall6.init)

*(.initcall7.init)

__initcall_end = .;

__con_initcall_start = .;

*(.con_initcall.init)

__con_initcall_end = .;

__security_initcall_start = .;

*(.security_initcall.init)

__security_initcall_end = .;

. = ALIGN(32);

__initramfs_start = .;

usr/built-in.o(.init.ramfs)

__initramfs_end = .;

. = ALIGN();

__per_cpu_start = .;

*(.data.percpu)

__per_cpu_end = .;

__init_begin = _stext;

*(.init.data)

. = ALIGN(4096);

__init_end = .;

}

/DISCARD/ : { /* Exit code and data */

*(.exit.text)

*(.exit.data)

*(.exitcall.exit)

}
这里的地址是 0xc0008000,它与内存的物理地址是如何联系起来的呢?

/* These are for everybody (although not all archs will actually

discard it in modules) */

#define __init __attribute__ ((__section__ (".init.text")))

#define __initdata __attribute__ ((__section__ (".init.data")))

#define __exitdata __attribute__ ((__section__(".exit.data")))

#define __exit_call __attribute_used__ __attribute__ ((__section__ (".exitcall.exit")))

#ifdef MODULE

#define __exit __attribute__ ((__section__(".exit.text")))

#else

#define __exit __attribute_used__ __attribute__ ((__section__(".exit.text")))

#endif

/* For assembly routines */

#define __INIT .section ".init.text

#define __FINIT .previous

#define __INITDATA .section ".init.data

#ifndef __ASSEMBLY__

/*

* Used for initialization calls..

*/

typedef int (*initcall_t)(void);

typedef void (*exitcall_t)(void);

extern initcall_t __con_initcall_start[], __con_initcall_end[];

extern initcall_t __security_initcall_start[], __security_initcall_end[];

/* Defined in init/main.c */

extern char saved_command_line[];

/* used by init/main.c */

extern void setup_arch(char **);

#endif
还有一个需要注意的是所有驱动模块的都对映一个 initcall_t的函数,这些函数的存放地址为__initcall_start,__initcall_end的一段空间中,而这段空间也正好在init段中。

3、__attribute__机制:GNU C的一大特色。可以设置函数属性,变量属性,类型属性等。

详细内容可以参考《GNU C __attribute__ 机制简介》。

二、跟文件系统的建立

(1)创建根文件系统的基本目录结构。

可以编写如下脚本:

#move the file to the directory where build the filesystem, and then run this shell

mkdir bin dev etc lib proc sbin sys tmp usr var

chmod 1777 tmp

mkdir usr/bin usr/lib usr/sbin

mkdir var/lib var/lock var/log var/run var/tmp

chmod 1777 var/tmp

#cd ./dev

# Don't use mknod ,unless you run this Script as root!

#mknod -m 600 console c 5 1

#mknod -m 666 null c 1 3
(2)配置、编译和安装Busybox,拷贝必需的动态库文件,

busybox可以编译静态二进制(这在运行时不需要动态链接库),不过最好还是动态编译,因为其它应用程序运行时往往需要动态链接库,又能节省空间。

注意:busybox是带有符点数的二进制文件,所以内核在配置时需要把-> Floating point emulation->NVFPE math emulation选上。

建立必需的符号链接。

ln -s busybox bin/ls

ln -s busybox sbin/init

ln -s busybox bin/cd

... ...
(3)修改和创建必要的文件。

(4)/sbin/init执行流程:

内核启动只后,最后会调用跟文件系统下的init可执行文件,进行一些配置工作。而init也正好是linux的第一个进程。

run_init_process("/sbin/init");

run_init_process("/etc/init");

run_init_process("/bin/init");

run_init_process("/bin/sh");

panic("No init found. Try passing init= option to kernel.");
流程大致如下:详细流程得参考busybox的init部分的程序。

a:首先运行/etc/init.d/rcS

/bin/echo "#mount all .............."

/bin/mount -a

/bin/echo /sbin/mdev > /proc/sys/kernel/hotplug

mdev -s

/bin/echo "****************************************"

/bin/echo " linux-2.6.18.1 nfs boot "

echo " 2009-07-26 "

echo "*****************************************"

echo
B : /etc/inittab

::sysinit:/etc/init.d/rcS

#s3c2410_serial0::respawn:/bin/sh

#::respawn:/bin/sh

::askfirst:-/bin/sh

tty2::askfirst:-/bin/sh

tty0::askfirst:-/bin/sh

tty3::askfirst:-/bin/sh

::restart:/sbin/init

::ctrlaltdel:/bin/reboot

::shutdown:/bin/umount -a -r

::shutdown:/sbin/swapoff -a
C: /etc/profile

echo -n "Processing /etc/profile....."

echo "set library path:..."

#set search library path

export LD_LIBRARY_PATH=/lib:/usr/lib

#set user path

PATH=/bin:/sbin:/usr/bin:/usr/sbin

export PATH

#Set PS1

USER="id-un"

LOGNAME=$USER

PS1='[\\\W]\\$'

PATH=$PATH

echo "Done..."
D: /etc/fstab

proc /proc proc defaults i 0 0

none /tmp ramfs defaults 0 0

mdev /dev ramfs defaults 0 0

sysfs /sys sysfs defaults 0 0
此外,还可以根据需要创建mdev.conf host,passwd,group...。

三、驱动模块的加载

下面举一个例子:在./driver/char/目录下建立hello模块,代码如下:

#include

#include

#include

static void hello_exit()

{

printk(KERN_ERR"HELLO EXIT\\n");

}

static int hello_init()

{

printk(KERN_ERR"HELLO init\\n");

}

module_init(hello_init);

module_exit(hello_exit);
在./driver/char/Makefile添加两行:

#obj-$(CONFIG_HELLO_MODULE) += hello.o

obj-y += hello.o
重新编译就能把新添加的hello模块添加到内核中了。

4.嵌入式Linux内核的编译和配置

●内核编译配置步骤

 1 下载所需的内核包,解压到/usr/src/下;

  2 cd到内核源码根目录下,运行make mrproper,清理上次编译的结果文件以及配置文件;[可选]

  3 进入到内核源码根目录下,运行make oldconfig,让内核按常用配置生成;

  - 这里面还有其他一些选项,如defconfig,是默认配置

  4 运行make menuconfig (需要ncurses软件包),这是在菜单模式下进行内核配置,可以选择将其编译built-in(设置为*)或是内核模块(设置为M);

  - 还有其他的菜单配置,如xconfig

  5 确认依赖性make dep; [可选]

  6 编译内核映像文件,make bzImage,这步大约耗时0.5~1小时;

  7 编译内核模块,之前选择为内核模块的组件现在被编译,执行make modules;

  8 安装内核模块,执行make modules_install,这步就是把编译好的内核模块放到/lib/modules/内核版本号/下,以便于新内核启动后加载对应的内核模块;

  9 建立要载入的内核映像文件,这步是把编好的内核放到/boot目录下,执行mkinitramfs -o /boot/initrd-linux2.6.XX.img 2.6.XX;

  10 安装内核,执行make install,将内核相关的文件全部复制到/boot下;

  11 最后一步,配置grub,使系统从新的内核引导。一般来说,这步因系统而异,在ubuntu10.04中需要修改/boot/grub/grub.cfg(以前的版本是修改menu.lst),语法规则很好懂,一看便知。

  需要注意的是,有时候改完了未必加载成功,可能仍然需要把系统根目录(即/)下的两个软链接vmlinuz和initrd.img一并修改为/boot下对应的新内核映像文件,才可以成功加载。

5.可以参考的资料

1. Building.Embedded.Linux.Systems,2nd,by OReilly.

2. Embedded Linux Primer: A Practical, Real-World Approach,by Prentice Hall

3.两个嵌入式Linux的Survey文件

4.其他网络资源下载本文

显示全文
专题