BIOS读取硬盘最前面512字节(MBR),MBR中只能存储一个OS的引导记录,如多系统则会出现问题
MBR包含部分或全部Bootloader及分区表
Bootloader一般包含两个阶段的代码
Bootloader模式:
自启动模式:从Flash等固态存储设备上将OS加载到RAM中运行,无需干预。
交互模式:通过串口或网络接口从Host上下载OS到RAM中运行,具备烧写等功能。
第一阶段:
第二阶段:
NFS服务:文件目录共享
TFTP服务:用TFTP协议下载
Minicom软件:串口通信,实现ioctl()方法
.config文件中
CONFIG_RAMDISK = y(yes) m(module) *(默认?)
make config
一行一行地进行参数配置
make menuconfig
以界面方式读Kconfig
make xconfig
以立体化gui的形式
cp arch/arm/boot/zImage /tftpboot
启动开发板,进入Bootloader
配置内核参数,配置Host/Target ip地址
tftp获取内核镜像文件
bootz引导内核
底层基本控制台驱动drivers/char/console.c
中间层驱动drivers/video/fbcon.c fbmem.c
顶层特定硬件的驱动程序radeonfb.c
设备文件/dev/fb0
,主设备号29
ioctl()可以获取屏幕参数
mmap将屏幕缓冲区映射到用户空间(传映射的空间大小)
读写操作用户空间地址
在嵌入式Linux应用中,主要的储存设备为RAM(DRAM,SDRAM)和ROM(常采用Flash存储器)。
Linux文件系统分为三类:
Flash闪存读写、擦除特点:写操作将1变成0,擦除操作将0恢复成1,擦除以块(block)为单位
NOR闪存:读速度快,价格高,支持XIP,以字为读写单位。
NAND闪存:写速度快,高存储密度,需特殊系统接口,以页面为单位读写。
在Flash之上,都需要MTD(Memory Technology Drivers)设备驱动支持,具体的文件系统如jffs2,yaffs等则建立在MTD之上,再之上则是VFS。
使用MTD驱动程序的主要优点在于,它是专门针对各种非易失性存储器(以闪存为主)而设计的,因而它对Flash有更好的支持、管理和基于扇区的擦除、读/写操作接口。
Ramdisk
将一部分固定大小的内存当作分区来使用,将一些经常被访问而又不会更改的文件(如只读的根文件系统)通过Ramdisk
放在内存中,可以明显地提高系统的性能。在Linux的启动阶段,initrd
提供了一套机制,可以将内核映像和根文件系统一起载入内存。
NFS,在不同机器、不同操作系统之间通过网络共享文件的技术。
NFS服务,要注意mount
命令使用方法
有操作系统时,硬件/驱动/应用程序的关系
操作系统通过给驱动制造麻烦来达到给上层应用提供便利的目的。当驱动都按照操作系统给出的独立于设备的接口而设计时,那么,应用程序将可使用统一的系统调用接口来访问各种设备。
设备驱动:顾名思义就是驱使硬件设备工作,设备驱动与底层硬件直接打交道,按照硬件设备的具体工作方式读写设备寄存器,完成设备的轮询、中断处理、DMA通信,进行物理内存向虚拟内存的映射,最终使通信设备能够收发数据,使显示设备能够显示文字和画面,使存储设备能够记录文件和数据。
Linux内核分为5大模块:
每块可以在运行时添加到内核的代码, 被称为一个模块. Linux 内核提供了对许多模块类型的支持, 包括但不限于, 设备驱动. 每个模块由目标代码组成( 没有链接成一个完整可执行文件 ), 可以动态连接到运行中的内核中, 通过 insmod
程序, 以及通过 rmmod
程序去连接。
大多数字符设备只提供顺序访问数据的能力,不像普通文件访问可以前后移动访问指针来存取不同位置上的数据。
“crw-rw-r–”第一个字母表示字符型设备,b代表block device(块设备)
块设备一般是大容量存储设备,需要建立文件系统来进行数据存储。块接口必须支持挂载文件系统。
网络接口设备的最小单位是数据包。
内核、用户地址空间是隔离开的,内核只能调用自身的代码。
内核模块中没有main
函数,内核中有main
函数
基于kbuild机制,编写Makefile
KSRC = /lib/modules/$(shell uname -r)/build
PWD = $(shell pwd)
obj-m = hello.o
default:
$(MAKE) -C $(KSRC) M=$(PWD) modules
clean:
S(MAKE) -C $(KSRC) SUBDIRS=$(PWD) clean
insmod
运行、加载
rmmod
卸载
lsmod
查看运行时模块列表
modinfo
查看内核模块信息
depmod -a
更新内核模块索引
modprobe
自动解决内核模块依赖
以Linux的方式看待设备,可区分为3种基本设备类型:字符设备、块设备和网络接口。其中,字符设备和块设备都在/dev
目录下可以找到相应的文件。
字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。文本控制台( /dev/console
)和串口( /dev/ttyS0
)是字符设备的例子, 因为它们很好地展现了流的抽象。字符设备通过文件系统结点来存取, 例如 /dev/tty1
和 /dev/lp0
。一个字符驱动常常至少实现 open
, close
, read
, 和 write
等系统调用。一个字符设备和一个普通文件之间唯一有关的不同就是, 普通文件可以自由移动,但大部分字符设备仅仅是数据通道, 只能顺序存取。设备文件是无文件大小的,取而代之的是两个号码:主设备号+此设备号。
块设备是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。如同字符设备, 块设备通过位于 /dev
目录的文件系统结点来存取。一个块设备(例如一个磁盘)应该是可以驻有一个文件系统的。**块和字符设备的区别仅仅在内核在内部管理数据的方式上, 因此在内核/驱动的软件接口上不同。**在大部分Unix-like系统中,一个块设备只能处理大小为2的整数倍的若干整块。但在Linux系统中,允许应用程序读写一个块设备像一个字符设备一样,即允许一次传送任意数目的字节。
任何网络事事务都通过一个接口来进行,就是说网络接口一个能够与其他主机交换数据的设备。一个网络接口可以是一个硬件设备,也可能是一个纯粹的软件设备,比如环回接口。一个网络接口负责发送和接收数据报文。尽管像TCP这样的网络是面向流的,但是网络设备却常常设计成处理报文的发送和接收,也就是说其对单个连接一无所知,它只处理报文,所以网络接口不是一个面向流的设备,一个网络接口就不像 /dev/tty1
那么容易映射到文件系统的一个结点上。Unix 提供的对接口的存取的方式仍然是通过分配一个名子给它们( 例如 eth0
),但是这个名子在文件系统中没有对应的入口,网络接口以配置文件的形式出现。
dmesg
看到打印输出的“Hello, world”,也可以用cat /proc/devices
obj-m
定义了要声明的模块,也就是.o文件,KSRC
定义了操作系统内核编译目录,M
指定了源文件的位置。Linux的内核编译采用Kbuild系统,这个Makefile文件将被调用两次,第一次是构造内核目录树,第二次是发现内核源码树后调用default
,来运行$(MAKE)
调用内核建立实际的模块建立工作。make
成功以后会在当前路径下生成.ko
文件。
以下两个头文件对于可加载模块是必须的
#include
#include
前者包含了大量加载模块需要的函数和符号的定义。后者指定初始化和清理函数。注意到在模块的最后我们使用了MODULE_AUTHOR("Sheng LYU")
和MODULE_LICENSE("GPL")
两个宏,前者定义了作者,后者定义了代码许可。
一般来说,主编号用来标识设备相连的驱动,比如/dev/null
和 /dev/zero
都由驱动 1 来管理,而虚拟控制台和串口终端都由驱动 4 管理。次编号被内核用来决定引用哪个设备。
sudo mknod /dev/test001 c 240 0
rm /dev/test001
大部分的基础性驱动包括 3 个重要的内核数据结构, 称为 file_operations
, file
, 和 inode
。file_operation
结描述构是一个字符驱动建立设备与编号的连接。scull 设备驱动只实现最重要的设备方法. 它的 file_operations 结构是如下初始化的:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
/* loff_t (*llseek) (struct file *, loff_t, int);
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).*/
.read = scull_read,
/*ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).*/
.write = scull_write,
/*ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.*/
.ioctl = scull_ioctl,
/*int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.*/
.open = scull_open,
/*int (*open) (struct inode *, struct file *);
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.*/
.release = scull_release,
/*int (*release) (struct inode *, struct file *);
在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.*/
};
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
/*major 是需要使用的主编号, name 是驱动的名子(出现在 /proc/devices), fops 是缺省的 file_operations 结构. 一个对 register_chrdev 的调用为给定的主编号注册 0 - 255 的次编号, 并且为每一个建立一个缺省的 cdev 结构. 使用这个接口的驱动必须准备好处理对所有 256 个次编号的 open 调用( 不管它们是否对应真实设备 ), 它们不能使用大于 255 的主或次编号.*/
int unregister_chrdev(unsigned int major, const char *name);
/*major 和 name 必须和传递给 register_chrdev 的相同, 否则调用会失败.*/
open
方法提供给驱动来做任何的初始化来准备后续的操作. 在大部分驱动中, open 应当进行下面的工作:
filp->private_data
的任何数据结构函数原型:
int (*open)(struct inode *inode, struct file *filp);
release
方法的角色是 open
的反面. 有时你会发现方法的实现称为 device_close
, 而不是 device_release
. Anyway, 设备方法应当进行下面的任务:
open
分配在 filp->private_data
中的任何东西close
关闭设备读和写方法都进行类似的任务, 就是, 从和到应用程序代码拷贝数据. 因此, 它们的原型相当相似, 可以同时介绍它们:
ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
对于 2 个方法, filp
是文件指针, count
是请求的传输数据大小. buff
参数指向持有被写入数据的缓存, 或者放入新数据的空缓存. 最后, offp 是一个指针指向一个"long offset type"对象, 它指出用户正在存取的文件位置。
需要注意的是,read
和 write
方法的 buff 参数是用户空间指针. 因此, 它不能被内核代码直接解引用.
显然, 你的驱动必须能够存取用户空间缓存以完成它的工作. 但是, 为安全起见这个存取必须使用特殊的, 内核提供的函数. 我们介绍几个这样的函数(定义于
), 剩下的在第一章"使用 ioctl 参数"一节中. 它们使用一些特殊的, 依赖体系的技巧来确保内核和用户空间的数据传输安全和正确.
scull 中的读写代码需要拷贝一整段数据到或者从用户地址空间. 这个能力由下列内核函数提供, 它们拷贝一个任意的字节数组, 并且位于大部分读写实现的核心中.
unsigned long copy_to_user(void __user *to,const void *from,unsigned long count);
//将数据从内核拷贝至用户空间,用于read操作
unsigned long copy_from_user(void *to,const void __user *from,unsigned long count);
//将数据从用户空间拷贝到内核中,用于write操作
大部分驱动需要 – 除了读写设备的能力 – 通过设备驱动进行各种硬件控制的能力. 大部分设备可进行超出简单的数据传输之外的操作; 用户空间必须常常能够请求, 例如, 设备锁上它的门, 弹出它的介质, 报告错误信息, 改变波特率, 或者自我销毁. 这些操作常常通过 ioctl 方法来支持, 它通过相同名子的系统调用来实现。
int ioctl(int fd, unsigned long cmd, ...); //应用层
long (*unlocked_ioctl)(struct file* file,
unsigned int cmd,
unsigned long arg);//内核层
copy_to_user
通常出现在设备驱动的读方法函数中(这里的读可以看成读内核,读内核后从内核中复制内容至用户空间)struct file_operations device_fops = {
llseek: device_llseek,
read: device_read,
write: device_write,
unlocked_ioctl: device_ioctl,
open: device_open,
release: device_release
};
或者
struct file_operations device_fops = {
.owner = THIS_MODULE,
.llseek = device_llseek,
.read = device_read,
.write = device_write,
.unlocked_ioctl = device_ioctl,
.open = device_open,
.release = device_release,
};
read, write
函数read
,但是还没有数据可读,此进程就必须阻塞。数据一到达,进程随即被唤醒,并把数据返回给调用者。read, write
函数read
,但是还没有数据可读,此进程就必须阻塞。数据一到达,进程随即被唤醒,并把数据返回给调用者。