韦东山第一二期衔接课程内容概要
- 0 使得一个裸板Jz2440能运行linux应用程序的过程
- 1 uboot启动内核总结
- 2 自己写bootloader
- 3 内核启动应用程序总结
- 4 根文件系统
- 5.1 字符设备驱动程序之概念介绍
- 5.2 字符设备驱动程序之LED驱动程序-编写编译
- 5.3 字符设备驱动程序之LED驱动程序-测试改进
- 5.4 字符设备驱动程序之LED驱动程序-操作LED
- 5.5 字符设备驱动程序之查询方式的按键驱动程序
- 5.6 字符设备驱动程序之中断方式的按键驱动程序_Linux异常处理结构
- 5.7 字符设备驱动程序之中断方式的按键驱动程序_Linux中断处理结构
- 5.8 字符设备驱动程序之中断方式的按键驱动程序_编写代码
- 5.9 字符设备驱动程序之poll机制
- 5.10 字符设备驱动程序之异步通知
- 查询、中断、poll、异步通知的优缺点
- 5.11 字符设备驱动程序之同步互斥阻塞
0 使得一个裸板Jz2440能运行linux应用程序的过程
首先得有一个u-boot。首先从uboot官网下载标准源码,然后打补丁,然后配置,最后编译,最终编译出来的文件是u-boot.bin,将这个文件通过PC的oflash工具烧写到单板中。由于u-boot中实现了串口功能,运行uboot后可通过串口进行操作,比如键入一些uboot命令。运行uboot后,可以通过uboot使用USB将内核烧写到单板的flash中,uboot的终极目的是将内核转到内存中,并跳转到内核入口程序,从而启动内核,内核启动后uboot就失去作用。
uboot通过USB烧写进flash的是内核编译好的映像文件,一般为uimage格式,这种格式是专门服务uboot的映像文件。首先从linux官网下载内核标准源码,然后打补丁,然后配置,最后编译,最后编译出来最好是uimage格式。通过uboot命令将内核编译好的uimage烧写到flash中,然后通过uboot命令启动内核。此时内核尚不能成功启动,因为还缺少根文件系统。
uboot的目的是启动内核,内核的目的是启动应用程序,应用程序位于根文件系统。内核想要启动应用程序分为两步,第一步首先要识别并挂接根文件系统,第二步才是启动应用程序。对于flash来说,擦除以后flash就是空的,内核的VFS会自动识别并挂接,并认为flash是默认的文件系统格式,只不过里面还没有具体内容。手动先在虚拟机上创建好根文件系统需要的各个文件,用文件映像工具将其转成对应格式的文件映像,将映像传到PC,最后通过uboot命令通过USB将文件映像烧写到单板flash的root分区中。
1 uboot启动内核总结
本段是对第一节的总结,总结一下从uboot的第一条指令的运行到Linux内核第一条指令的运行,这中间的过程,具体可以看下面的各个小节。
uboot的本质是启动内核,为了达成这一目标,需要完成一些功能,比如读写flash,读写sdram、使用串口、网卡、usb等。
uboot的代码分为两个阶段,汇编阶段和C语言阶段,其中C语言阶段还可以分为C第一阶段和C第二阶段。
汇编阶段,uboot的第一条指令位于u-boot-1.1.6\cpu\arm920t\start.S,从bl reset开始,关看门狗、屏蔽中断、设置栈、初始化时钟、重定位、清bss段等,汇编阶段最后,会调整PC值,转到C函数start_armboot,至此,汇编阶段结束。
C语言第一阶段,在汇编阶段中只是做了一些最底层的硬件初始化,并没有使能具体外设,C语言第一阶段就是编写C程序,使得uboot具有了操作flash、sdram、串口、网卡、USB等功能,这部分没有仔细去看代码。
C语言第二阶段,会设置uboot的默认环境变量,代码在u-boot-1.1.6\common\environment.c中,我们可以修改源代码来改变环境变量,也可以在uboot串口界面通过set命令改变。
随后会将内核启动所需要的参数以tag的形式存放到固定位置,这个位置每个单板不同,启动参数中主要包括内存的大小,命令行信息等,其中命令行信息包含根文件系统位于flash的分区,init启动的第一个程序,控制台是谁,这些都在环境变量bootargs中:
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
此时uboot已经可以在串口显示信息,会开始倒数计时。如果计时结束之前按下空格,则会进入一个死循环,uboot循环等待串口读入命令,然后执行相应的命令,有关uboot命令的具体实现我没怎么看。如果在计时结束之前没按空格,则uboot直接运行下面的程序:
s=getenv("bootcmd")
printf("Booting Linux ...\n");
run_command(s,0);
uboot的核心就是run_command各种命令,这里bootcmd是uboot的环境变量,里面存放两条uboot命令:
nand read.jffs2 0x3000F7C0 kernel;bootm 0x3000F7C0
nand read.jffs2 0x3000F7C0命令从外存从将内核映像文件读入sdram的特定位置
bootm命令调用lib_arm/armlinux.c中的do_bootm_linux函数,在该函数中调用:
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
ntohl(hdr->ih_ep)是内核第一条代码的地址,bd->bi_arch_number是机器ID,单板相关,是自己写死的, bd->bi_boot_params是内核启动参数的存放首地址,也是单板相关,也是写死了。
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);就会转到执行内核第一条代码,至此整个流程结束。
1.1 u-boot分析之编译体验
u-boot本身是一个集合众多处理器架构的启动文件,文件很大,我们某一款芯片用到的是其中一小部分。如何在庞大的u-boot文件集中正确选到我们需要的呢(比如我们的arm920t架构下的s3c2440),这需要编写makefile。u-boot的makefile也很大,但并不是所有的makefile语句都会被执行,其中有很多条件语句,真正某一款芯片下makefile执行的语句可能很少。
除此之外,u-boot并不是全能的,它并不能完全具体到某一块板子,会缺少一些很具体的东西,这些需要我们以补丁的形式加在u-boot中使得其支持我们的板子。怎么阅读补丁文件以及怎么通过命令将补丁打在源代码上,在本节视频5min处。
将U-boot源代码包以及补丁包放在PC的Linux系统上。他那个patch文件是怎么弄出来的?
u-boot的终极目的是启动内核,也就是跳转到内核入口。在此之前它需要首先将内核代码的映像文件,比如uimage烧写到flash中(这需要uboot有写flash的功能),然后需要将内核代码从外存(嵌入式中一般为flash,桌面PC中一般为硬盘)中读出,然后写入内存(SDRAM),除此之外,还需要打印调试信息,所以需要有串口读写功能,如果想通过USB烧写内核代码,还需要支持USB。可以简单总结一下uboot的功能:
1、初始化时钟、看门狗、SDRAM、flash、串口、USB、网卡等
2、读写SDRAM、读写flash
1.2 u-boot分析之Makefile结构分析
通过分析u-boot的makefile去分析其代码结构。
移植u-boot的配置阶段需要自己创建并编写的文件:
u-boot-1.1.6/include/configs/<board_name>.h
u-boot-1.1.6/ board /board_name
移植u-boot的配置阶段需要自己修改的原有文件:
u-boot-1.1.6/Makefile
移植u-boot的配置阶段系统自动生成的文件:
u-boot-1.1.6/include/config.mk
u-boot-1.1.6/include/config.h
问:u-boot-1.1.6/include/configs/<board_name>.h以及u-boot-1.1.6/ board /board_name的具体内容应该怎么写?
1.3 u-boot分析之源码第一阶段
对于我们的单板,uboot运行的第一个文件是u-boot-1.1.6\cpu\arm920t\start.S,这是从Makefile里面分析出来的。
start.S中前几条还是各种中断向量跳转,第一条是bl reset。reset中做以下操作:
1、通过CPSR寄存器设置cpu为管理模式(svc)
2、关看门狗
3、屏蔽所有中断
4、初始化SDRAM
5、设置管理模式的栈(C程序的运行需要栈,栈是跳转到第二阶段的c程序必不可少的)
6、初始化时钟
7、根据链接脚本进行代码重定位
8、清BSS段
9、设置pc的值,转到C函数start_armboot
ldr pc, _start_armboot
_start_armboot: .word start_armboot
1.4 u-boot分析之源码第二阶段
进入start_armboot后,调用一系列c函数,具体实现第一阶段未完成的功能,比如实现flash、sdram、网卡、USB、串口等功能,初始化环境变量,设置机器ID,设置内核启动参数。
获取内核启动依赖的环境变量bootcmd,bootcmd里面包含了两条uboot命令
s=getenv("bootcmd")
倒数计时,如果在uboot倒数计时结束前没按空格,则运行bootcmd中的uboot命令:
printf("Booting Linux ...\n");
run_command(s,0);
倒数计时,如果按下空格,则陷入死循环,等待用户键入命令,然后
run_command(键入的命令);
uboot的核心就是各种命令,以及run_command函数,启动内核的实现也是通过命令,bootcmd。
1.5 u-boot分析之u-boot命令实现
u-boot的核心在于命令,视频里演示了如何编写一个新的命令,以及在u-boot中加入一个新的文件以后如果修改对应的Makefile
1.6 u-boot分析之u-boot启动内核
首先讲解了有关nand flash中分区的一些概念,每个分区的大小是在源码中写死了的。具体源码在u-boot-1.1.6/include/configs/<board_name>.h。一般分区是:bootloader到uboot环境参数到内核映像再到文件系统。具体信息可在u-boot命令界面输入mtd命令查看。
内核启动过程依赖于环境变量bootcmd中的两个命令,uboot界面输入print命令可以看到环境变量,通过set命令可以改变环境变量的值,set后键入命令save保存,然后键入reset命令重启uboot。
nand read.jffs2 0x3000F7C0 kernel;bootm 0x3000F7C0
nand read.jffs2 0x3000F7C0 kernel命令将内核从nand flash的kernel分区(kernel代表了nandflash中的某个起始地址以及长度)中读到SDRAM中的 0x3000F7C0。这里的内核是UImage格式的,整个文件的前64字节为内核头部,头部中记录了内核加载地址和入口地址,加载地址就是整个内核文存放在SDRAM的哪个位置,这里就是 0x3000F7C0,入口地址就是真正内核代码的第一条执行地址,即0x3000F7C0加上头部的64字节,入口地址为0x30008000,这个入口地址是由开发板决定的,0x3000F7C0是0x30008000减去64字节人为决定的。
有一个问题在于,编译好的内核映像是通过u-boot的USB下载功能烧写到nand的kernel分区的,u-boot是怎么准确将内核烧写到kernel分区的呢?
bootm 0x3000F7C0命令检查内核的真正代码是否在入口地址了,如果没有就移动内核到入口地址,这也是为什么我们要人为设定0x3000F7C0的原因,这样就不同再移动了。随后该命令会设置内核的启动参数,随后调到入口地址开始运行内核。最终启动是调用do_bootm_linux函数来启动,该函数在lib_arm/armlinux.c中,其所做的事情如下:
1、设置内核启动所需的参数
由于内核一旦运行,uboot就不会运行了,两者无法实时通信,解决方法是uboot事先在某地址按某种格式存放内核的启动参数。这个某个地址对于我们单板来说是0x30000100,某种格式是tag格式,tag是标记,每种类型的参数都是一个tag,所有tag汇集成标记列表。简单说一下内核启动参数的种类,注意把内核启动参数和uboot的环境变量区分开,每种类型的启动参数都是一个tag,如setup_start_tag、setup_mem_tags、setup_commandline_tag、setup_end_tag等,
开始tag,表示内核启动参数的开始
static void setup_start_tag (bd_t *bd)
{
params = (struct tag *) bd->bi_boot_params;//表示该tag的首地址,在u-boot-1.1.6\board\100ask24x0中,
//bi_boot_params被我们设置为0x30000100
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size (tag_core);
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next (params);//指向下一个tag的首地址
}
内存tag,PC机启动时,BIOS能自动检测机器的内存大小,对于嵌入式开发,Linux内核也需要知道内存的大小,我们这里不用像BIOS那样自动检测,直接写死一个内存大小就行。
static void setup_memory_tags (bd_t *bd)
{
int i;
for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = bd->bi_dram[i].start;
params->u.mem.size = bd->bi_dram[i].size;
//上面的start和siz规定了内存的起始地址和大小,我们的板子是写好了的,
//地址是0x30000000,大小是0x04000000,也就是64MB。
params = tag_next (params);
}
}
命令行tag,函数参数中的commandline也在lib_arm/armlinux.c中赋值:
char *commandline = getenv ("bootargs");
commandline就是uboot环境变量中的bootargs,在uboot等待页面键入print命令,得到bootargs中信息如下:
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
bootargs中以空格隔开了四个等式,root=/dev/mtdblock3表示根文件系统位于flash的第四个分区(从0开始),这里注意我们的flash并不像PC机上的磁盘那样有分区表,flash是没有分区表的,分区是在程序中写死了的,具体源码在u-boot-1.1.6/include/configs/<board_name>.h,一般分区是:bootloader到uboot环境参数到内核映像再到文件系统,具体信息可在u-boot命令界面输入mtd命令查看;init=/linuxrc表示第一个应用程序是/linuxrc;console=ttySAC0表示控制台为串口0,也就是打印信息打到串口0。
static void setup_commandline_tag (bd_t *bd, char *commandline)
{
char *p;
if (!commandline)
return;
/* eat leading white space */
for (p = commandline; *p == ' '; p++);
/* skip non-existent command lines so the kernel will still
* use its default command line.
*/
if (*p == '\0')
return;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size =
(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;
strcpy (params->u.cmdline.cmdline, p);
params = tag_next (params);
}
结束tag,表示内核启动参数的结束
static void setup_end_tag (bd_t *bd)
{
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
2、调到内核入口地址
void (*theKernel)(int zero, int arch, uint params);//theKernel是一个函数指针
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);//ntohl(hdr->ih_ep)表示从内核映像uimage的头部中取出入口地址,
//该地址当作一个指针赋给theKernel
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);//执行theKernel所指向的c函数,也就是内核的第一条代码所在的函数
//bd->bi_arch_numbers是机器ID,2440和2410有不同的ID,该ID在100ask24x0.c中由我们赋值,
//内核通过比较机器ID来判断能否支持该单板,
//bd->bi_boot_params是内核启动参数存放的首地址
至此,整个流程完毕,我们在uboot等待界面中键入boot命令,就会执行环境变量bootcmd中的两个命令,最后启动内核。
2 自己写bootloader
没看,老视频,1和3才是连贯的
3 内核启动应用程序总结
3.1 内核启动流程分析之编译体验
视频讲解实际操作,如何打补丁,如何配置,如何编译。注意如果要将内核编译成uImage的话,需要先编译u-boot,在u-boot的tools下将mkimage复制到虚拟机上的/usr/local/bin目录下,再进行make uImage。编译好的uImage在:
/home/zwg/MyShare1/linux-2.6.22.6/arch/arm/boot目录下
3.2 内核启动流程分析之配置
3.3 内核启动流程分析之Makefile
3.4 内核启动流程分析之内核启动
u-boot的终极目的是启动内核,内核的终极目的是运行应用程序。应用程序在根文件系统里面。要重点关注u-boot程序的运行起点和终点、内核的运行起点和终点。本节内容可以结合韦东山的书。
从1.6节我们知道,uboot最后通过theKernel (0, bd->bi_arch_number, bd->bi_boot_params);来执行内核的代码,这句话传入两个参数,机器ID和tag参数的首地址。内核运行后第一件事显然是处理uboot传过来的tag参数,然后做一系列的初始化,然后挂接根文件系统(内核的最终目的是执行应用程序,应用程序在根文件系统中),最后执行应用程序。
内核启动也可以分为两个阶段,汇编和C阶段,内核启动运行的第一个文件是arch\arm\kernel\head.S,该文件就是汇编阶段。在这个文件中做以下事情:
1、首先判断是否支持这个CPU(是否支持该ARM体系架构的cpu,判断方法未知),然后根据uboot传过来的机器ID判断能否支持该处理器(是否支持该SOC,比如s3c2440,视频讲了很久,有空可以看一下,底层很奇妙的东西)
2、创建一级页表,bl __create_page_tables,create_page_tables是个标签,随后使能MMU,设置栈等,详细请看书
3、跳转到start_kernel函数,b start_kernel,start_kernel就是内核的第一个C函数。至此head.S的任务完成,head.S处理了uboot传给内核的两个参数中的机器ID,没有处理tag参数,tag参数将由C函数来处理。
下面是C阶段的处理:
start_kernel函数,在linux-2.6.22.6\init\main.c中,该函数做以下事情:
1、首先做一些初始化,如中断初始化等
2、打印Linux内核版本信息
3、调用两个函数处理uboot传给内核的tag参数:
setup_arch(&command_line);
setup_command_line(command_line);
4、一系列初始化,如调度初始化,控制台初始化等,想一想运行应用程序需要什么功能,这里就要初始化什么
5、start_kernel函数的最后,调用rest_init()函数。
rest_init函数,同样位于linux-2.6.22.6\init\main.c中,该函数做以下事情:
1、创建一个内核线程,暂且认为相当于调用kernel_init这个函数
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
2、kernel_init函数调用prepare_namespace(),prepare_namespace调用mount_root,mount_root函数挂接根文件系统
3、prepare_namespace函数调用成功后,kernel_init函数调用init_post函数,在init_post函数中打开控制台dev/console,执行根文件系统中的应用程序:
/*将标准输入、输出、错误都定位到控制台(我们在uboot中设置为串口0)*/
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
/*如果我们在uboot环境变量bootargs中设置了init=xxx,则首先运行xxx程序
*比如我们这里是init=/linuxrc,则首先运行linuxrc
*/
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
。。。
/*如果我们在uboot环境变量bootargs中没有设置init=xxx,则往下依次运行下面四个程序
* 注意,这些应用程序一般都会有死循环,也就是说一但运行了某个程序,一般就不会返回,
* 所以不是下面四个都会运行,只会运行第一个能运行的程序
*/
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
总结一下C函数阶段的处理顺序:
1、一系列初始化,包括中断、调度、控制台等,这里的中断应该不是ARM书中的那个中断了
2、处理uboot传过来的tag参数
3、创建内核线程kernel_init,该内核线程挂接根文件系统、打开控制台、执行根文件系统中的应用程序,自此内核启动完成。
问题:视频最后,讲了root=/dev/mtdblock3,讲了flash分区的概念,可是之前视频说分区在u-boot-1.1.6/include/configs/<board_name>.h中写死,而这个视频说在linux-2.6.22.6\arch\arm\plat-s3c24xx\Common_smdk.c中。
4 根文件系统
4.1 构建根文件系统之启动第1个程序
对于嵌入式而言,内核运行时一般全部代码位于内存中,也就是SDRAM中,而内核想要执行的程序存放在根文件系统中,文件系统都应该在外存中,也就是nand flash中。
视频讲解了如何用u-boot擦除uImage带有的文件系统,也就是如何擦除nand,还讲解了如何烧写新的文件系统到开发板。
4.2 构建根文件系统之init进程分析
视频介绍了busybox的功能;介绍了init进程的功能。在视频的最后讲到,不管是init进程还是busybox,本质都是应用程序,它们都依赖于C库。init进程本身就是busybox。内核在运行最后一般会执行/sbin/init程序,这个程序是busybox提供的,里面就实现了init进程的功能。/sbin/init程序所实现的init进程是后续所有进程的发起者和控制者。
一个最小的根文件系统需要哪些东西:(1)/dev/console /dev/null (2)init进程所基于的具体程序,一般在/sbin/init中(来源于busybox) (3)配置文件/etc/initab (4)配置文件指定的应用程序 (5)C库
所谓构造根文件系统,实际上就是构造上面五个部分。
4.3 构建根文件系统之busybox
介绍在虚拟机上配置,编译以及安装busybox,注意安装时不要直接 make install,可以在虚拟机上创建一个目录来存放根文件系统,busybox也安装到该目录下。busybox安装完成后,会自动在该目录创建bin、sbin、usr/bin、usr/sbin等目录。接下来我们需要手动在该目录创建/dev/console 、/dev/null等设备文件以及/etc/initab等配置文件,创建命令为mknod,除此之外还需要创建/lib目录并将C库中的动态链接文件放到lib中,这些都是4.4的内容。
具体操作:将资料提供的打好补丁的busybox压缩文件复制到/home/zwg/MyShare1,解压文件到当前文件夹,进入解压后的目录:
/home/zwg/MyShare1/busybox-1.7.0_patched,安装书上进行配置和编译,最后安装,安装之前先建立一个目录:/home/zwg/MyShare1/nfs_root/first_fs,将busybox安装到这个目录下,安装后该目录自动生成bin、sbin、usr/bin、usr/sbin等目录
对于嵌入式系统来说,前面3.4run_init_process的初始程序一般是busybox中的init.c,具体在busybox-1.7.0_patched\init\init.c。
在该文件中主要做以下操作:
1、在init_main函数中调用parse_inittab函数解析inittab,从而决定后续执行什么程序
在parse_inittab函数中,file = fopen(INITTAB, “r”); 打开配置文件,INITTAB就是/etc/inittab。
2、/etc/inittab在后续自己构建根文件系统时由自己编写,编写按照一定格式:
:::
id:id会加上dev前缀,/dev/id,表示控制台
runlevels:运行级别,一般忽略
action:执行时机,也就是什么时候执行,其取值有sysinit,respawn,askfirst,wait,once,restart,ctrlaltdel,shutdown等,具体可以看韦东山的书
process:可执行的程序,应用程序或者脚本
3、parse_inittab函数中会首先解析出/etc/inittab,然后调用new_init_action函数,该函数原型如下,三个参数分别对应inittab中的action,process,id。
static void new_init_action(int action, const char *command, const char *cons)
new_init_action函数首先创建一个init_action结构:
struct init_action {
struct init_action *next;
int action;
pid_t pid;
char command[INIT_BUFFS_SIZE];
char terminal[CONSOLE_NAME_SIZE];
};
然后将init_action结构用传给new_init_action函数的参数进行填充,填充后放入init_action_list链表,我们解析完整个inittab后,链表里就有我们所有需要运行的程序的信息了。
4、得到init_action_list链表后,回到init_main函数,开始执行所有需要执行的程序:
/* Now run everything that needs to be run */
/* First run the sysinit command */
run_actions(SYSINIT);//先执行action是sysinit的inittab程序
/* Next run anything that wants to block */
run_actions(WAIT);
/* Next run anything to be run only once */
run_actions(ONCE);
。。。
/* Now run the looping stuff for the rest of forever */
while (1) {
/* run the respawn stuff */
run_actions(RESPAWN);
/* run the askfirst stuff */
run_actions(ASKFIRST);
/* Don't consume all CPU time -- sleep a bit */
sleep(1);
/* Wait for a child process to exit */
wpid = wait(NULL);
while (wpid > 0) {
/* Find out who died and clean up their corpse */
for (a = init_action_list; a; a = a->next) {
if (a->pid == wpid) {
/* Set the pid to 0 so that the process gets
* restarted by run_actions() */
a->pid = 0;
message(L_LOG, "process '%s' (pid %d) exited. "
"Scheduling it for restart.",
a->command, wpid);
}
}
/* see if anyone else is waiting to be reaped */
wpid = waitpid(-1, NULL, WNOHANG);
}
}
5、对于run_actions函数:
static void run_actions(int action)
{
struct init_action *a, *tmp;
for (a = init_action_list; a; a = tmp) {
tmp = a->next;
if (a->action == action) { //比较链表中的哪一项和传入的action相同
/* a->terminal of "" means "init's console" */
if (a->terminal[0] && access(a->terminal, R_OK | W_OK)) {
delete_init_action(a);
} else if (a->action & (SYSINIT | WAIT | CTRLALTDEL | SHUTDOWN | RESTART)) {
waitfor(a, 0); //执行action对应的应用程序,并等待它执行完毕,waitfor里面是用waitpid实现的
//waitpid在《现代操作系统》418有说
delete_init_action(a); //由于该应用程序已经执行完毕了,所以将它从init_action_list中删掉
//以SYSINIT | WAIT | CTRLALTDEL | SHUTDOWN | RESTART为action的应用程序
//在最开始就执行,并且内核会等待其执行结束,执行完一次就删掉
} else if (a->action & ONCE) {
run(a); //run函数通过fork创建子进程去执行a中的程序,上面的waitfor里面也有run函数
//可以深入看看run,看看父子进程如何运行的,子进程如何调用EXEC执行具体代码的
delete_init_action(a);
//对于once的程序,内核不会等待它执行完成,同样也是执行一次就删掉
} else if (a->action & (RESPAWN | ASKFIRST)) {
/* Only run stuff with pid==0. If they have
* a pid, that means it is still running */
if (a->pid == 0) {
a->pid = run(a);
}
}
}
}
}
上面讲了action为
4.4 构建根文件系统之构建根文件系统
具体介绍怎么创建/dev/console (打印标准输入输出和错误)、/dev/null(/dev/null表示如果在inittab中没设置id,那么标准输入输出和错误就定位到/dev/null)等设备文件以及/etc/initab等配置文件、介绍怎么安装C库、介绍怎么制作yaffs2映像文件。
具体操作:在虚拟机上随意人为创建目录:
/home/zwg/MyShare1/nfs_root/first_fs
在该目录中创建/dev/console、/dev/null、/etc/initab、/lib
编写/etc/initab,先写一个简单的,比如console::askfirst:-/bin/sh,这样内核会运行一个shell程序,这个程序是busybox自带的。
将C库中的.so文件复制到/lib中,C库在资料光盘中的gcc-3.4.5-glibc-2.3.6中
利用资料提供的工具将整个/home/zwg/MyShare1/nfs_root/first_fs变为yaffs2映像文件。
将制作好的yaffs2映像文件放到PC,烧写到单板。
视频后面还讲了书上17.4.1、17.4.2、17.4.5
对于以上的方法,我们每次在虚拟机上修改文件系统后,都需要重新制作映像文件,重新烧写到单板,这样太麻烦了。视频的最后,讲解了如何制作网络文件系统NFS。
所谓NFS,是指将文件系统放在服务器上,内核启动时通过网络从服务器中识别出对应目录,把它当作根文件系统。
5.1 字符设备驱动程序之概念介绍
驱动也是内核的一部分,毕竟驱动程序在运行之前需要编译进内核中,实际上Linux内核就是由各种驱动组成的,内核源码中大概85%都是各种驱动。内核中的驱动程序种类齐全,在实际开发时可以在同类型驱动的基础上进行修改以符合具体开发板。编写驱动程序的难点并不是硬件的具体操作,二是弄清现有驱动程序的框架,在这个框架中加入这个硬件。
编写一个Linux设备驱动程序的大致流程:《嵌入式Linux开发完全手册》P386
对于字符驱动程序,具体在于file_operations结构体,每一个字符驱动程序都有一个对应的file_operations结构体。每个驱动程序在注册时向内核提供所属设备类型和主设备号,以供应用程序找到对应的驱动,次设备号用于驱动程序自身辨别它自己是同类设备的第几个,这个同类设备是指字符设备大类下再细分的小类。
5.2 字符设备驱动程序之LED驱动程序-编写编译
视频内容:编写了字符驱动的基本框架,但是这个驱动是在虚拟机上的Linux系统中的驱动,不是嵌入式板子上的Linux系统的驱动。注意一下视频末尾如何编译驱动程序,编译完成后形成xxx.ko文件,然后将ko文件复制到/home/zwg/MyShare1/nfs_root/first_fs,制作映像文件,烧写到单板,启动系统,在串口上执行insmod,即可将驱动加载到单板上的内核,加载后可用cat /proc/devices命令查看是否有驱动对应的设备文件,有则表明驱动注册成功。注意:这里显示有对应的主设备号并不意味着真的有这个设备节点,在5.3还需要根据这个主设备号用mknod创建真正的设备节点。
5.3 字符设备驱动程序之LED驱动程序-测试改进
视频写了一个测试的C程序,编译成可执行程序后放到单板运行,在单板串口上通过printk打印一句话。这里一个需要弄清楚的问题在于:驱动程序和测试程序之间的交互方法。
方法一:驱动程序中用register_chrdev注册时人为确定一个主设备号,比如111,这个主设备号不能和cat /proc/devices中看到的重复;测试程序中用open(“/dev/xxx”, O_RDWR)时,需要人为在单板系统上通过mknod 建立一个名字为/dev/xxx的设备节点,设置其主设备号111,否者测试程序无法和驱动程序联系上。
方法二:驱动程序中用register_chrdev注册时主设备号写0,系统会自动创建一个不重复的主设备号,可以用 cat /proc/devices查看;测试程序中用open(“/dev/xxx”, O_RDWR)时,需要人为在单板系统上通过mknod 建立一个名字为/dev/xxx的设备节点,设置其主设备号为自动分配的那个主设备号,否者测试程序无法和驱动程序联系上。
方法三:前两种方法都需要程序员去查看cat /proc/devices,然后再mknod,这样太麻烦。创建具体设备节点的工作可以由mdev完成,mdev根据内核中的系统信息自动创建对应的设备节点,我们需要在驱动程序中提供这些mdev需要的系统信息。mdev能够实时检测驱动的加载与卸载,从而实时生成与销毁对应的设备节点,这个功能是mdev的热拔插功能,具体看视频。
5.4 字符设备驱动程序之LED驱动程序-操作LED
要想写一个具体能够点亮LED灯的驱动程序,分为几步。
第一步:完善硬件的操作,包括:看原理图、看2440手册、以及写代码。其中写代码需要注意几点。第一是我们以前写单片机程序是直接操纵的实际的物理地址,但是我们现在单板上运行着linux系统,有了MMU,我们现在需要操纵虚拟地址。需要用ioremap函数将物理地址映射为虚拟地址;第二是用户空间的参数传递给内核空间的具体驱动需要用到copy_from_user、copy_to_user等函数,它不像普通的函数调用的参数传递过程。
注意:这节课的例程里有main(int argc, char **argv)的用法,值得体会。
在视频19min以前讲的是如何通过编写驱动和测试程序,使得能够通过测试程序开关所有的三个led灯,这个过程只用到了主设备号,没用到次设备号。视频的后半部分讲解如何通过次设备号开关三个led灯中的某一个,次设备号完全由开发者定义其意义,次设备号为几代表什么意思,完全通过编写驱动程序决定。具体看程序注释
5.5 字符设备驱动程序之查询方式的按键驱动程序
和5.4类似,只看了下程序,没看视频。
5.6 字符设备驱动程序之中断方式的按键驱动程序_Linux异常处理结构
视频讲的不是很清楚,推荐直接看书。主要关注IRQ的处 理,对于irq需要关注几个点:
(1)在Linux刚启动时,异常是禁止的,注意Linux如何重新设置异常向量表,在trap.c中。我疑惑的是为什么中断向量的虚拟地址是0xffff0000,cpu怎么知道要从0xffff0000这个虚拟地址取中断向量,这个虚拟地址怎么转换成实际地址。
(2)注意如何从异常向量表,调用到irq.c中的asm_do_IRQ,它才是真正开始处理irq中断的函数,之前都是在用宏实现保存现场、计算返回地址等操作。
5.7 字符设备驱动程序之中断方式的按键驱动程序_Linux中断处理结构
15min以前的视频都是在内核里面各种跳,没必要看。15min开始讲解按下一个按键后到中断处理的整体流程。23min开始讲解开发者如何将自己写的中断处理函数注册到内核中,主要讲述request_irq、setup_irq以及free_irq。节省时间可以直接看33min开始的总结部分。推荐直接看书。
request_irq函数用于注册中断函数,函数参数为:中断号、中断处理函数、触发方式、设备名字、设备ID。
5.8 字符设备驱动程序之中断方式的按键驱动程序_编写代码
视频开头讲解了有关中断号的内容,中断号与具体芯片有关,内核里面已经定义好了,具体可以看书406页。具体看程序注释
这一节的程序在third_drv文件夹。整个12衔接的所有视频,都是只有一个线程,用到的不管是查询、中断、poll以及信号,都是中断上下文和用户空间程序的交互手段。单纯的通过中断来交互:
(1)用户程序调用read函数
fd = open("/dev/buttons", O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
}
while (1)
{
read(fd, &key_val, 1);//注意这里需要一直读,不过好在读是阻塞状态的
printf("key_val = 0x%x\n", key_val);
}
(2)驱动的read中,阻塞方式,如果没有按键按下,就休眠
ssize_t third_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
if (size != 1)
return -EINVAL;
/* 如果没有按键动作, 休眠 */
wait_event_interruptible(button_waitq, ev_press);
/* 如果有按键动作, 返回键值 */
copy_to_user(buf, &key_val, 1);
ev_press = 0;
return 1;
}
(3)中断中如果有按键按下,就唤醒
static irqreturn_t buttons_irq(int irq, void *dev_id)
{
struct pin_desc * pindesc = (struct pin_desc *)dev_id;
unsigned int pinval;
pinval = s3c2410_gpio_getpin(pindesc->pin);
if (pinval)
{
/* 松开 */
key_val = 0x80 | pindesc->key_val;
}
else
{
/* 按下 */
key_val = pindesc->key_val;
}
ev_press = 1; /* 表示中断发生了,这是wait_event_interruptible必须的 */
wake_up_interruptible(&button_waitq); /* 唤醒休眠的进程 */
return IRQ_RETVAL(IRQ_HANDLED);
}
wait_event_interruptible的使用:https://blog.csdn.net/allen6268198/article/details/8112551
中断方法在视频中说还是需要应用程序主动通过read查询,这样不太好,其实不尽然。我们这里是单个线程,如果多个线程,比如线程A通过read读取按键,阻塞了,那么线程A会放弃cpu进入等待队列,这样其他线程就有执行的机会。哪怕就是我们这个程序,看似只有一个线程,实际上包括shell以及swap线程等等,我们的线程阻塞了并不意味着CPU一直被我们的线程占用。
5.9 字符设备驱动程序之poll机制
代码在forth_drv文件夹。
视频11min讲解了在不用运行测试程序的情况下,通过直接打开对应的设备文件,来触发相应的驱动程序。
上一节用中断方式是read如果一直没读到数据,就一直阻塞,也就是说这个线程会一直在等待队列,一直得不到执行,我们这里当然没问题,因为这个线程本来也没啥事。但是如果实际应用中,如果无法忍受线程一直被某个IO操作阻塞,要么选择以非阻塞方式访问,要么可以用poll机制。
poll机制的含义是推迟,比如如果线程调用read,如果马上读取到了数据,自然直接返回,如果没读取到数据,线程会阻塞,但是不会一直阻塞,比如阻塞5秒内如果还没被中断唤醒(表明有数据),则到5秒后自动唤醒并返回,这就是poll机制。
sleep(5);//注意sleep和wait的区别,sleep不会放弃CPU的
//https://www.linuxprobe.com/sleep-wait.html
5.10 字符设备驱动程序之异步通知
查询、中断以及poll机制,都是应用程序主动的不间断的去查询驱动程序中的中断信息,有没有办法让中断发生时,驱动程序主动通知应用程序,应用程序收到通知后,再去查询驱动程序,这种方法叫异步通知,用信号(signal)来实现。具体看程序注释
个人感觉还是异步通知更好,如果不带Linux,单板的中断程序自己就可以处理数据,中断程序和主程序都可以看作用户程序的一部分,不需要主程序休眠。而加了Linux以后,由于我们的程序都运行在用户态,而中断是内核态,这样中断不能直接代替用户程序完成工作,所以会出现即使用了中断,仍需要用户程序休眠的操作,因为必须要用户程序来处理某些东西。异步通知相当于是在中断中再次中断,达成的效果就类似我们在单板中的所用的中断的效果了。
查询、中断、poll、异步通知的优缺点
查询:极度耗资源,CPU占用率99
中断:read函数陷入
5.11 字符设备驱动程序之同步互斥阻塞
目的:同一时刻只能有一个应用程序打开按键驱动程序,也就是打开/dev/buttons。讲解了原子操作、信号量、阻塞等知识的具体操作,注意信号量和前面异步通知里面的信号是两回事。视频前面的例子挺好的,有关原子性、共享设备的保护,操作系统的多任务性,体会一下。视频里面那个笔记可以多看下。