在筆者撰寫的《C語言嵌入式系統編程修煉之道》一文中,主要陳訴的軟件架構是單任務無操作系統平臺的,而本文的側重點則在於講述操作系統嵌入的軟件架構,二者的區別如下圖:
嵌入式操作系統並不總是必須的,因爲程序完全可以在裸板上運行。儘管如此,但對於複雜的系統,爲使其具有任務管理、定時器管理、存儲器管理、資源管理、 事件管理、系統管理、消息管理、隊列管理和中斷處理的能力,提供多任務處理,更好的分配系統資源的功能,很有必要針對特定的硬件平臺和實際應用移植操作系 統。鑑於Linux的源代碼開放性,它成爲嵌入式操作系統領域的很好選擇。國內外許多知名大學、公司、研究機構都加入了嵌入式Linux的研究行列,推出 了一些著名的版本:
·RT-Linux提供了一個精巧的實時內核,把標準的Linux核心作爲實時核心的一個進程同用戶的實時進程一 起調度。RT-Linux已成功地應用於航天飛機的空間數據採集、科學儀器測控和電影特技圖像處理等廣泛的應用領域。如NASA(美國國家宇航局)將裝有 RT-Linux的設備放在飛機上,以測量Georage咫風的風速;
·uCLinux(Micro-Control-Linux,u表示Micro,C表示Control)去掉了MMU(內存管理)功能,應用於沒有虛擬內存管理的微處理器/微控制器,它已經被成功地移植到了很多平臺上。
本章涉及的mizi-linux由韓國mizi公司根據Linux 2.4內核移植而來,支持S3C2410A處理器。
1.Linux內核要點
和其他操作系統一樣,Linux包含進程調度與進程間通信(IPC)、內存管理(MMU)、虛擬文件系統(VFS)、網絡接口等,下圖給出了Linux的組成及其關係:
Linux內核源代碼包括多個目錄:
(1)arch:包括硬件特定的內核代碼,如arm、mips、i386等;
(2)drivers:包含硬件驅動代碼,如char、cdrom、scsi、mtd等;
(3)include:通用頭文件及針對不同平臺特定的頭文件,如asm-i386、asm-arm等;
(4)init:內核初始化代碼;
(5)ipc:進程間通信代碼;
(6)kernel:內核核心代碼;
(7)mm:內存管理代碼;
(8)net:與網絡協議棧相關的代碼,如ipv4、ipv6、ethernet等;
(9)fs:文件系統相關代碼,如nfs、vfat等;
(10)lib:庫文件,與平臺無關的strlen、strcpy等,如在string.c中包含:
char * strcpy(char * dest,const char *src) { char *tmp = dest; while ((*dest++ = *src++) != '\0') /* nothing */; return tmp; } |
(11)Documentation:文檔
在Linux內核的實現中,有一些數據結構使用非常頻繁,對研讀內核的人來說至爲關鍵,它們是:
1.task_struct
Linux內核利用task_struct數據結構代表一個進程,用task_struct指針形成一個task數組。當建立新進程的時候,Linux 爲新的進程分配一個task_struct結構,然後將指針保存在task數組中。調度程序維護current指針,它指向當前正在運行的進程。
2.mm_struct
每個進程的虛擬內存由mm_struct結構代表。該結構中包含了一組指向vm-area_struct結構的指針,vm-area_struct結構描述了虛擬內存的一個區域。
3.inode
Linux虛擬文件系統中的文件、目錄等均由對應的索引節點(inode)代表。
2.Linux移植項目
mizi-linux已經根據Linux 2.4內核針對S3C2410A這一芯片進行了有針對性的移植工作,包括:
(1)修改根目錄下的Makefile文件
a.指定目標平臺爲ARM:
#ARCH := $(shell uname -m | sed -e s/i.86/i386/ -e s/sun4u/sparc64/ -e s/arm.*/arm/ -e s/sa110/arm/) ARCH := arm |
b.指定交叉編譯器:
CROSS_COMPILE = arm-linux-
(2)修改arch目錄中的文件
根據本章第一節可知,Linux的arch目錄存放硬件相關的內核代碼,因此,在Linux內核中增加對S3C2410的支持,最主要就是要修改arch目錄中的文件。
a.在arch/arm/Makefile文件中加入:
ifeq ($(CONFIG_ARCH_S3C2410),y) TEXTADDR = 0xC0008000 MACHINE = s3c2410 Endif |
b.在arch\arm\config.in文件中加入:
if [ "$CONFIG_ARCH_S3C2410" = "y" ]; then comment 'S3C2410 Implementation' dep_bool ' SMDK (MERI TECH BOARD)' CONFIG_S3C2410_SMDK $CONFIG_ARCH_S3C2410 dep_bool ' change AIJI' CONFIG_SMDK_AIJI dep_tristate 'S3C2410 USB function support' CONFIG_S3C2410_USB $CONFIG_ARCH_S3C2100 dep_tristate ' Support for S3C2410 USB character device emulation' CONFIG_S3C2410_USB_CHAR $CONFIG_S3C2410_USB fi # /* CONFIG_ARCH_S3C2410 */ |
arch\arm\config.in文件還有幾處針對S3C2410的修改。
c.在arch/arm/boot/Makefile文件中加入:
ifeq ($(CONFIG_ARCH_S3C2410),y) ZTEXTADDR = 0x30008000 ZRELADDR = 0x30008000 endif |
d.在linux/arch/arm/boot/compressed/Makefile文件中加入:
ifeq ($(CONFIG_ARCH_S3C2410),y) OBJS += head-s3c2410.o endif |
加入的結果是head-s3c2410.S文件被編譯爲head-s3c2410.o。
e.加入arch\arm\boot\compressed\ head-s3c2410.S文件
#include <linux/config.h> #include <linux/linkage.h> #include <asm/mach-types.h> .section ".start", #alloc, #execinstr __S3C2410_start: @ Preserve r8/r7 i.e. kernel entry values @ What is it? @ Nandy @ Data cache, Intstruction cache, MMU might be active. @ Be sure to flush kernel binary out of the cache, @ whatever state it is, before it is turned off. @ This is done by fetching through currently executed @ memory to be sure we hit the same cache bic r2, pc, #0x1f add r3, r2, #0x4000 @ 16 kb is quite enough... 1: ldr r0, [r2], #32 teq r2, r3 bne 1b mcr p15, 0, r0, c7, c10, 4 @ drain WB mcr p15, 0, r0, c7, c7, 0 @ flush I & D caches #if 0 @ disabling MMU and caches mrc p15, 0, r0, c1, c0, 0 @ read control register bic r0, r0, #0x05 @ disable D cache and MMU bic r0, r0, #1000 @ disable I cache mcr p15, 0, r0, c1, c0, 0 #endif /* * Pause for a short time so that we give enough time * for the host to start a terminal up. */ mov r0, #0x00200000 1: subs r0, r0, #1 bne 1b |
該文件中的彙編代碼完成S3C2410特定硬件相關的初始化。
f.在arch\arm\def-configs目錄中增加配置文件
g.在arch\arm\kernel\Makefile中增加對S3C2410的支持
no-irq-arch := $(CONFIG_ARCH_INTEGRATOR) $(CONFIG_ARCH_CLPS711X) \ $(CONFIG_FOOTBRIDGE) $(CONFIG_ARCH_EBSA110) \ $(CONFIG_ARCH_SA1100) $(CONFIG_ARCH_CAMELOT) \ $(CONFIG_ARCH_S3C2400) $(CONFIG_ARCH_S3C2410) \ $(CONFIG_ARCH_MX1ADS) $(CONFIG_ARCH_PXA) obj-$(CONFIG_MIZI) += event.o obj-$(CONFIG_APM) += apm2.o |
h.修改arch/arm/kernel/debug-armv.S文件,在適當的位置增加如下關於S3C2410的代碼:
#elif defined(CONFIG_ARCH_S3C2410) .macro addruart,rx mrc p15, 0, \rx, c1, c0 tst \rx, #1 @ MMU enabled ? moveq \rx, #0x50000000 @ physical base address movne \rx, #0xf0000000 @ virtual address .endm .macro senduart,rd,rx str \rd, [\rx, #0x20] @ UTXH .endm .macro waituart,rd,rx .endm .macro busyuart,rd,rx 1001: ldr \rd, [\rx, #0x10] @ read UTRSTAT tst \rd, #1 << 2 @ TX_EMPTY ? beq 1001b .endm |
i.修改arch/arm/kernel/setup.c文件
此文件中的setup_arch非常關鍵,用來完成與體系結構相關的初始化:
void __init setup_arch(char **cmdline_p) { struct tag *tags = NULL; struct machine_desc *mdesc; char *from = default_command_line; ROOT_DEV = MKDEV(0, 255); setup_processor(); mdesc = setup_machine(machine_arch_type); machine_name = mdesc->name; if (mdesc->soft_reboot) reboot_setup("s"); if (mdesc->param_offset) tags = phys_to_virt(mdesc->param_offset); /* * Do the machine-specific fixups before we parse the * parameters or tags. */ if (mdesc->fixup) mdesc->fixup(mdesc, (struct param_struct *)tags, &from, &meminfo); /* * If we have the old style parameters, convert them to * a tag list before. */ if (tags && tags->hdr.tag != ATAG_CORE) convert_to_tag_list((struct param_struct *)tags, meminfo.nr_banks == 0); if (tags && tags->hdr.tag == ATAG_CORE) parse_tags(tags); if (meminfo.nr_banks == 0) { meminfo.nr_banks = 1; meminfo.bank[0].start = PHYS_OFFSET; meminfo.bank[0].size = MEM_SIZE; } init_mm.start_code = (unsigned long) &_text; init_mm.end_code = (unsigned long) &_etext; init_mm.end_data = (unsigned long) &_edata; init_mm.brk = (unsigned long) &_end; memcpy(saved_command_line, from, COMMAND_LINE_SIZE); saved_command_line[COMMAND_LINE_SIZE-1] = '\0'; parse_cmdline(&meminfo, cmdline_p, from); bootmem_init(&meminfo); paging_init(&meminfo, mdesc); request_standard_resources(&meminfo, mdesc); /* * Set up various architecture-specific pointers */ init_arch_irq = mdesc->init_irq; #ifdef CONFIG_VT #if defined(CONFIG_VGA_CONSOLE) conswitchp = &vga_con; #elif defined(CONFIG_DUMMY_CONSOLE) conswitchp = &dummy_con; #endif #endif } |
j.修改arch/arm/mm/mm-armv.c文件(arch/arm/mm/目錄中的文件完成與ARM相關的MMU處理)
修改
init_maps->bufferable = 0; |
爲
init_maps->bufferable = 1; |
要輕而易舉地進 行上述馬拉松式的內核移植工作並非一件輕鬆的事情,需要對Linux內核有很好的掌握,同時掌握硬件特定的知識和相關的彙編。幸而mizi公司的開發者們 已經合力爲我們完成了上述工作,這使得小弟們在將mizi-linux移植到自身開發的電路板的過程中只需要關心如下幾點:
(1)內核初始化:Linux內核的入口點是start_kernel()函數。它初始化內核的其他部分,包括捕獲,IRQ通道,調度,設備驅動,標定延遲循環,最重要的是能夠fork"init"進程,以啓動整個多任務環境。
我們可以在init中加上一些特定的內容。
(2)設備驅動:設備驅動佔據了Linux內核很大部分。同其他操作系統一樣,設備驅動爲它們所控制的硬件設備和操作系統提供接口。
本文第四章將單獨講解驅動程序的編寫方法。
(3)文件系統:Linux最重要的特性之一就是對多種文件系統的支持。這種特性使得Linux很容易地同其他操作系統共存。文件系統的概念使得用戶能 夠查看存儲設備上的文件和路徑而無須考慮實際物理設備的文件系統類型。Linux透明的支持許多不同的文件系統,將各種安裝的文件和文件系統以一個完整的 虛擬文件系統的形式呈現給用戶。
我們可以在K9S1208 NAND FLASH上移植cramfs、jfss2、yaffs等FLASH文件系統。
3. init進程
在init函數中"加料",可以使得Linux啓動的時候做點什麼,例如廣州友善之臂公司的demo板在其中加入了公司信息:
static int init(void * unused) { lock_kernel(); do_basic_setup(); prepare_namespace(); /* * Ok, we have completed the initial bootup, and * we're essentially up and running. Get rid of the * initmem segments and start the user-mode stuff.. */ free_initmem(); unlock_kernel(); if (open("/dev/console", O_RDWR, 0) < 0) printk("Warning: unable to open an initial console.\n"); (void) dup(0); (void) dup(0); /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ printk("========================================\n"); printk("= Friendly-ARM Tech. Ltd. =\n"); printk("= http://www.arm9.net =\n"); printk("= http://www.arm9.com.cn =\n"); printk("========================================\n"); if (execute_command) execve(execute_command,argv_init,envp_init); execve("/sbin/init",argv_init,envp_init); execve("/etc/init",argv_init,envp_init); execve("/bin/init",argv_init,envp_init); execve("/bin/sh",argv_init,envp_init); panic("No init found. Try passing init= option to kernel."); } |
這樣在Linux的啓動過程中,會額外地輸出:
======================================== = Friendly-ARM Tech. Ltd. = = http://www.arm9.net = = http://www.arm9.com.cn = ======================================== |
4.文件系統移植
文件系統是基於被劃分的存儲設備上的邏輯上單位上的一種定義文件的命名、存儲、組織及取出的方法。如果一個Linux沒有根文件系統,它是不能被正確的啓動的。因此,我們需要爲Linux創建根文件系統,我們將其創建在K9S1208 NAND FLASH上。
Linux的根文件系統可能包括如下目錄(或更多的目錄):
(1)/bin (binary):包含着所有的標準命令和應用程序;
(2)/dev (device):包含外設的文件接口,在Linux下,文件和設備採用同種地方法訪問的,系統上的每個設備都在/dev裏有一個對應的設備文件;
(3)/etc (etcetera):這個目錄包含着系統設置文件和其他的系統文件,例如/etc/fstab(file system table)記錄了啓動時要mount 的filesystem;
(4)/home:存放用戶主目錄;
(5)/lib(library):存放系統最基本的庫文件;
(6)/mnt:用戶臨時掛載文件系統的地方;
(7)/proc:linux提供的一個虛擬系統,系統啓動時在內存中產生,用戶可以直接通過訪問這些文件來獲得系統信息;
(8)/root:超級用戶主目錄;
(9)/sbin:這個目錄存放着系統管理程序,如fsck、mount等;
(10)/tmp(temporary):存放不同的程序執行時產生的臨時文件;
(11)/usr(user):存放用戶應用程序和文件。
採用BusyBox是縮小根文件系統的好辦法,因爲其中提供了系統的許多基本指令但是其體積很小。衆所周知,瑞士軍刀以其小巧輕便、功能衆多而聞名世界,成爲各國軍人的必備工具,並廣泛應用於民間,而BusyBox也被稱爲嵌入式Linux領域的"瑞士軍刀"。
此地址可以下載BusyBox:http://www.busybox.net,當前最新版本爲1.1.3。編譯好busybox後,將其放入/bin目錄,若要使用其中的命令,只需要建立link,如:
ln -s ./busybox ls
ln -s ./busybox mkdir
4.1 cramfs
在根文件系統中,爲保護系統的基本設置不被更改,可以採用cramfs格式,它是一種只讀的閃存文件系統。製作cramfs文件系統的方法爲:建立一個 目錄,將需要放到文件系統的文件copy到這個目錄,運行"mkcramfs 目錄名 p_w_picpath名"就可以生成一個cramfs文件系統的p_w_picpath文件。例如如果目錄名爲rootfs,則正確的命令爲:
mkcramfs rootfs rootfs.ramfs
我們使用下面的命令可以mount生成的rootfs.ramfs文件,並查看其中的內容:
mount -o loop -t cramfs rootfs.ramfs /mount/point
此地址可以下載mkcramfs工具:http://sourceforge.net/projects/cramfs/。
4.2 jfss2
對於cramfs閃存文件系統,如果沒有ramfs的支持則只能讀,而採用jfss2(The Journalling Flash File System version 2)文件系統則可以直接在閃存中讀、寫數據。jfss2 是一個日誌結構(log-structured)的文件系統,包含數據和原數據(meta-data)的節點在閃存上順序地存儲。jfss2記錄了每個擦 寫塊的擦寫次數,當閃存上各個擦寫塊的擦寫次數的差距超過某個預定的閥值,開始進行磨損平衡的調整。調整的策略是,在垃圾回收時將擦寫次數小的擦寫塊上的 數據遷移到擦寫次數大的擦寫塊上以達到磨損平衡的目的。
與mkcramfs類似,同樣有一個mkfs.jffs2工具可以將一個目錄製作爲jffs2文件系統。假設把/bin目錄製作爲jffs2文件系統,需要運行的命令爲:
mkfs.jffs2 -d /bin -o jffs2.img
4.3 yaffs
yaffs 是一種專門爲嵌入式系統中常用的閃存設備設計的一種可讀寫的文件系統,它比jffs2 文件系統具有更快的啓動速度,對閃存使用壽命有更好的保護機制。爲使Linux支持yaffs文件系統,我們需要將其對應的驅動加入到內核中 fs/yaffs/,並修改內核配置文件。使用我們使用mkyaffs工具可以將NAND FLASH中的分區格式化爲yaffs格式(如/bin/mkyaffs /dev/mtdblock/0命令可以將第1個MTD塊設備分區格式化爲yaffs),而使用mkyaffsp_w_picpath(類似於mkcramfs、 mkfs.jffs2)則可以將某目錄生成爲yaffs文件系統鏡像。
嵌入式Linux還可以使用NFS(網絡文件系統)通過以太網 掛接根文件系統,這是一種經常用來作爲調試使用的文件系統啓動方式。通過網絡掛接的根文件系統,可以在主機上生成ARM 交叉編譯版本的目標文件或二進制可執行文件,然後就可以直接裝載或執行它,而不用頻繁地寫入flash。
採用不同的文件系統啓動時,要注意通過第二章介紹的BootLoader修改啓動參數,如廣州友善之臂的demo提供如下三種啓動方式:
(1)從cramfs掛接根文件系統:root=/dev/bon/2();
(2)從移植的yaffs掛接根文件系統:root=/dev/mtdblock/0;
(3)從以太網掛接根文件系統:root=/dev/nfs。
5.小結
本章介紹了嵌入式Linux的背景、移植項目、init進程修改和文件系統移植,通過這些步驟,我們可以在嵌入式系統上啓動一個基本的Linux。