第六、七章 嵌入式Linux开发

第六、七章 嵌入式Linux开发

BIOS读取硬盘最前面512字节(MBR),MBR中只能存储一个OS的引导记录,如多系统则会出现问题

MBR包含部分或全部Bootloader及分区表

Bootloader一般包含两个阶段的代码

Bootloader模式:

自启动模式:从Flash等固态存储设备上将OS加载到RAM中运行,无需干预。

交互模式:通过串口或网络接口从Host上下载OS到RAM中运行,具备烧写等功能。

Bootloader功能

第一阶段:

  • 初始化基本硬件
  • 把Bootloader自动搬运到内存中
  • 设置堆栈指针并将bss段清零。为后续执行代码做准备。

第二阶段:

  • 初始化本阶段要用到的硬件
  • 读取环境变量
  • 启动
  • 自启动模式,从Flash或通过网络加载内核并执行
  • 交互下载模式,接收到用户的命令后执行相关操作

嵌入式Linux开发环境

NFS服务:文件目录共享

TFTP服务:用TFTP协议下载

Minicom软件:串口通信,实现ioctl()方法

.config文件中

CONFIG_RAMDISK = y(yes) m(module) *(默认?)

make config一行一行地进行参数配置

make menuconfig以界面方式读Kconfig

make xconfig以立体化gui的形式

Linux内核引导

cp arch/arm/boot/zImage /tftpboot

启动开发板,进入Bootloader

配置内核参数,配置Host/Target ip地址

tftp获取内核镜像文件

bootz引导内核

FrameBuffer驱动程序

底层基本控制台驱动drivers/char/console.c

中间层驱动drivers/video/fbcon.c fbmem.c

顶层特定硬件的驱动程序radeonfb.c

设备文件/dev/fb0,主设备号29

ioctl()可以获取屏幕参数

mmap将屏幕缓冲区映射到用户空间(传映射的空间大小)

读写操作用户空间地址

Linux文件系统

在嵌入式Linux应用中,主要的储存设备为RAM(DRAM,SDRAM)和ROM(常采用Flash存储器)。

Linux文件系统分为三类:

  • 基于Flash的文件系统(实体)
  • 基于ARM的文件系统(内存)
  • 网络文件系统NFS

Flash文件系统

Flash闪存读写、擦除特点:写操作将1变成0,擦除操作将0恢复成1,擦除以块(block)为单位

NOR闪存:读速度快,价格高,支持XIP,以字为读写单位。

NAND闪存:写速度快,高存储密度,需特殊系统接口,以页面为单位读写。

在Flash之上,都需要MTD(Memory Technology Drivers)设备驱动支持,具体的文件系统如jffs2,yaffs等则建立在MTD之上,再之上则是VFS。

使用MTD驱动程序的主要优点在于,它是专门针对各种非易失性存储器(以闪存为主)而设计的,因而它对Flash有更好的支持、管理和基于扇区的擦除、读/写操作接口。

RAM file system

Ramdisk将一部分固定大小的内存当作分区来使用,将一些经常被访问而又不会更改的文件(如只读的根文件系统)通过Ramdisk放在内存中,可以明显地提高系统的性能。在Linux的启动阶段,initrd提供了一套机制,可以将内核映像和根文件系统一起载入内存。

Network file system

NFS,在不同机器、不同操作系统之间通过网络共享文件的技术。

NFS服务,要注意mount命令使用方法

Linux的设备驱动程序

有操作系统时,硬件/驱动/应用程序的关系

操作系统通过给驱动制造麻烦来达到给上层应用提供便利的目的。当驱动都按照操作系统给出的独立于设备的接口而设计时,那么,应用程序将可使用统一的系统调用接口来访问各种设备。

设备驱动:顾名思义就是驱使硬件设备工作,设备驱动与底层硬件直接打交道,按照硬件设备的具体工作方式读写设备寄存器,完成设备的轮询、中断处理、DMA通信,进行物理内存向虚拟内存的映射,最终使通信设备能够收发数据,使显示设备能够显示文字和画面,使存储设备能够记录文件和数据。

Linux内核架构

第六、七章 嵌入式Linux开发_第1张图片

Linux内核分为5大模块:

  • 进程管理(Process Scheduler):负责管理CPU资源,以便让各个进程可以以尽量公平的方式访问CPU
  • 内存管理(Memory Management):负责管理内存资源,以便让各个进程可以安全地共享机器的内存资源。另外,内存管理会提供虚拟内存的机制,该机制可以让进程使用多于系统可用的内存,不用的内存会通过文件系统保存在外部非易失存储器中,需要使用的时候,再取回到内存中。
  • 虚拟文件系统(Virtual File System, VFS):内核将不同功能的外部设备抽象为可以通过统一的文件操作接口
  • 网络(Network):负责管理系统的网络设备,并实现多种多样的网络标准
  • 设备控制(Device Control):几乎每个系统操作最终都映射到一个物理设备上. 除了处理器, 内存和非常少的别的实体之外, 全部中的任何设备控制操作都由特定于要寻址的设备相关的代码来进行. 这些代码称为设备驱动。

每块可以在运行时添加到内核的代码, 被称为一个模块. 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 目录下可以找到相应的文件。

  • 字符设备(Character Device)

字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。文本控制台( /dev/console )和串口( /dev/ttyS0 )是字符设备的例子, 因为它们很好地展现了流的抽象。字符设备通过文件系统结点来存取, 例如 /dev/tty1/dev/lp0。一个字符驱动常常至少实现 open, close, read, 和 write 等系统调用。一个字符设备和一个普通文件之间唯一有关的不同就是, 普通文件可以自由移动,但大部分字符设备仅仅是数据通道, 只能顺序存取。设备文件是无文件大小的,取而代之的是两个号码:主设备号+此设备号。

  • 块设备(Block Devices)

块设备是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。如同字符设备, 块设备通过位于 /dev 目录的文件系统结点来存取。一个块设备(例如一个磁盘)应该是可以驻有一个文件系统的。**块和字符设备的区别仅仅在内核在内部管理数据的方式上, 因此在内核/驱动的软件接口上不同。**在大部分Unix-like系统中,一个块设备只能处理大小为2的整数倍的若干整块。但在Linux系统中,允许应用程序读写一个块设备像一个字符设备一样,即允许一次传送任意数目的字节。

  • 网络接口

任何网络事事务都通过一个接口来进行,就是说网络接口一个能够与其他主机交换数据的设备。一个网络接口可以是一个硬件设备,也可能是一个纯粹的软件设备,比如环回接口。一个网络接口负责发送和接收数据报文。尽管像TCP这样的网络是面向流的,但是网络设备却常常设计成处理报文的发送和接收,也就是说其对单个连接一无所知,它只处理报文,所以网络接口不是一个面向流的设备,一个网络接口就不像 /dev/tty1 那么容易映射到文件系统的一个结点上。Unix 提供的对接口的存取的方式仍然是通过分配一个名子给它们( 例如 eth0 ),但是这个名子在文件系统中没有对应的入口,网络接口以配置文件的形式出现

  • 可以调用dmesg看到打印输出的“Hello, world”,也可以用cat /proc/devices

内核模块和应用程序的区别

  • 应用程序是从头到尾按顺序执行,而内核模块中的函数是被动调用的。
  • **内核模块不可以调用C库。**因为内核模块仅仅被连接到内核,因此模块所能调用的函数仅仅是由内核导出的那些函数。
  • 内核模块必须要做一些清理性的工作。应用程序在退出时可以不去释放在其调用前申请的资源,而模块在退出时必须要确定其加载时所申请的一切资源,否则,在系统重启之前它所申请的资源将一直残留系统之中。
  • **应用程序运行在用户空间,而内核模块运行在内核空间。**在Unix下,内核在最高级别运行,在这个级别任何事情都被允许,而应用程序在最低级运行,这里处理器控制了对硬件的直接存取以及对内存的非法存取。

编译和加载模块的过程

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, 和 inodefile_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和release方法

open 方法提供给驱动来做任何的初始化来准备后续的操作. 在大部分驱动中, open 应当进行下面的工作:

  • 检查设备特定的错误(例如设备没准备好, 或者类似的硬件错误)
  • 如果它第一次打开, 初始化设备
  • 如果需要, 更新 f_op 指针.
  • 分配并填充要放进 filp->private_data 的任何数据结构

函数原型:

int (*open)(struct inode *inode, struct file *filp);

release 方法的角色是 open 的反面. 有时你会发现方法的实现称为 device_close, 而不是 device_release. Anyway, 设备方法应当进行下面的任务:

  • 释放 open 分配在 filp->private_data 中的任何东西
  • 在最后的 close 关闭设备

read和write方法

读和写方法都进行类似的任务, 就是, 从和到应用程序代码拷贝数据. 因此, 它们的原型相当相似, 可以同时介绍它们:

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"对象, 它指出用户正在存取的文件位置。

需要注意的是,readwrite 方法的 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接口

大部分驱动需要 – 除了读写设备的能力 – 通过设备驱动进行各种硬件控制的能力. 大部分设备可进行超出简单的数据传输之外的操作; 用户空间必须常常能够请求, 例如, 设备锁上它的门, 弹出它的介质, 报告错误信息, 改变波特率, 或者自我销毁. 这些操作常常通过 ioctl 方法来支持, 它通过相同名子的系统调用来实现。

int ioctl(int fd, unsigned long cmd, ...); //应用层
long (*unlocked_ioctl)(struct file* file,
                      unsigned int cmd,
                      unsigned long arg);//内核层

习题

  • mknod不能创建普通文件。c 字符文件,p管道文件
  • 函数copy_to_user通常出现在设备驱动的读方法函数中(这里的读可以看成读内核,读内核后从内核中复制内容至用户空间)
  • GNU make在软件开发过程中的作用是在程序编译阶段,根据依赖关系减少编译和链接的过程,提高编译效率
  • 一个进程启动时默认打开3个文件:标准输入、标准输出、标准出错处理(stdin、stdout、stderr)
  • inode节点和文件不是一一对应的,软链接的inode号不一样,硬链接的inode是一样的。

第六、七章 嵌入式Linux开发_第2张图片

第六、七章 嵌入式Linux开发_第3张图片

第六、七章 嵌入式Linux开发_第4张图片

  • 设备驱动是上层应用和底层硬件的接口,提供访问硬件的机制。
  • 上层应用通过文件系统/dev下的设备节点文件来访问设备,触发内核中对应的设备驱动进行工作
  • Linux内核通过设备的主设备号来找到对应的设备驱动模块
  • 设备驱动模块通过次设备号来区分某一大类设备中的细分设备
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,
};
  • llseek()方法是对lseek()系统调用的具体实现,用于完成设备的定位操作
  • ioctl()方法,除了读写设备之外,设备驱动程序还需要提供各种各样的硬件控制能力,例如配置设备、进入或退出某种操作模式,一般不通过read/write等文件操作来完成,例如串口通信协议(波特率、数据格式等)的设置。在Linux内核中,使用unlocked_ioctl()方法来实现ioctl()系统调用。
  • int ioctl(int fd, int cmd, …)fd设备文件号,cmd命令,第三个参数可以指向各种数据类型。
  • 大容量数据传输还得用read, write函数
  • 如果一个进程调用了read,但是还没有数据可读,此进程就必须阻塞。数据一到达,进程随即被唤醒,并把数据返回给调用者。
  • 如果数据没有就绪,则简单的返回-EAGAIN,因此,非阻塞型操作会立即返回。
  • 设备驱动提供mechanism(机制),应用程序提供policy(策略)
  • /proc是一种特殊的伪文件系统,内核利用它向外界输出信息。/proc下面的每个文件都被绑定到一个内核函数,这个函数在文件被读取时,动态的生成文件的“内容”
    ux内核中,使用unlocked_ioctl()方法来实现ioctl()系统调用。
  • int ioctl(int fd, int cmd, …)fd设备文件号,cmd命令,第三个参数可以指向各种数据类型。
  • 大容量数据传输还得用read, write函数
  • 如果一个进程调用了read,但是还没有数据可读,此进程就必须阻塞。数据一到达,进程随即被唤醒,并把数据返回给调用者。
  • 如果数据没有就绪,则简单的返回-EAGAIN,因此,非阻塞型操作会立即返回。
  • 设备驱动提供mechanism(机制),应用程序提供policy(策略)
  • /proc是一种特殊的伪文件系统,内核利用它向外界输出信息。/proc下面的每个文件都被绑定到一个内核函数,这个函数在文件被读取时,动态的生成文件的“内容”
  • 用户空间对指针的错误使用通常会导致’Segment Fault‘,在内核空间对内存地址的非法使用一般会导致“oops”

你可能感兴趣的:(微机原理与嵌入式Linux编程,linux,运维,服务器,嵌入式硬件)