内核复习提纲

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

⏹内核空间
◆对于提供保护机制的现代系统来说,内核独立于普通应用程序,它一般处于
系统态,拥有受保护的内存空间和访问硬件设备的所有权限。

这种系统态和
被保护起来的内存空间,统称为内核空间。

⏹用户空间
◆应用程序在用户空间执行。

它们只能看到允许它们使用的部分系统资源,并
且不能使用某些特定的系统功能,不能直接访问硬件,还有其他一些使用限
制。

当内核运行的时候,系统以内核态进入内核空间,相反,普通用户程序以用户态进入用户空间
⏹进程上下文
◆当一个应用程序请求执行一条系统调用,我们说内核正在代其执行。

进一步
解释,应用程序被称为通过系统调用在内核空间运行,而内核被称为运行于
进程上下文中。

这种交互关系——应用程序通过系统调用陷入内核——是应用程序完成其工作的基本行为方式。

⏹中断上下文
◆许多操作系统的中断服务程序都不在进程上下文中执行。

它们在一个与所有
进程都无关的、专门的中断上下文中运行。

◆这些上下文代表着内核活动的范围。

概括为下列三者之一:
☐运行于内核空间,处于进程上下文,代表某个特定的进程执行。

☐运行干内核空间,处于中断上下文,与任何进程无关,处理某个特
定的中断。

☐运行于用户空间,执行用户进程。

配置编译内核:
$ tar zxvf linux-4.4.19.tar.gz
在编译内核之前,首先你必须配置它。

由于内核提供了数不胜数的功能,支持了难以计数的硬件,因而有许多东西需要配置。

这些配置项要么是二选一,要么是三选一。

配置选项也可以是字符串或整数。

⏹内核提供了各种不同的工具来简化内核配置。

◆最简单的一种是一个基于文本的命令行工具:$make config
☐该工具会挨个遍历所有配置项,要求用户选择yes、no或是module(如
果是三选一的话)。

◆用基于ncurse库的图形界面工具:$make menuconfig
◆用基于x11的图形工具:$make xconfig
◆用基于gtk+图形工具:$make gconfig
编译内核:
配置完成后保存
$ make -j2 V=1
编译完后得到linux内核: arch/arm/boot/zImage
内核开发的特点:
◆内核编程时不能访问C库。

◆内核编程时必须使用GNU C。

◆内核编程时缺乏像用户空间那样的内存保护机制。

◆内核编程时浮点数很难使用。

◆内核只有一个很小的定长堆栈。

◆由于内核支持异步中断、抢占和SMP,因此必须时刻注意同步和并发。

◆要考虑可移植性的重要性。

Kconfig:配置菜单中的配置选项是来源与Kconfig文件的,内核源码的每个子目录下都有Kconfig文件,负责提供菜单选项;
Menuconfig:配置菜单.
.config:配置好后,保存退出配置菜单,所有的配置选项会保存在.config隐藏文件中;Makefile:内核源码每个子目录下都有一个Makefile负责该目录下的文件编译工作;
Makefile中的目标:
obj-m += xxx.o表示xxx.c文件编译成模块
obj-y+=xxx.o表示xxx.c文件编译进内核映像文件uImage中
什么是系统调用:
由操作系统实现提供的所有系统调用所构成的集合即程序接
口或应用编程接口(API),是应用程序同系统之间的接口。

系统调用处理过程:
用户空间的程序无法直接执行内核代码。

所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。

通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序
内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝。

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定。

如果执行失败,这两个函数返回的都是没能完成拷贝的数据的字节数。

如果成功,返回0。

当出现上述错误时,系统调用返回标准-EFAULT。

添加系统调用:
1.1 在内核源码的arch/arm/kernel/sys_arm.c文件中添加一个
系统调用的实现;
gedit arch/arm/kernel/sys_arm.c
在文件最后添加如下代码,这个就是系统调用的实现代码:
asmlinkage long sys_foo(void)
{
printk("my syscallsys_foo!\n");
return THREAD_SIZE;
}
1.2 将系统调用添加到系统调用表中:
gedit arch/arm/kernel/calls.S
在系统调用号为377后面添加,如下蓝色部分
/* 375 */ CALL(sys_setns)
CALL(sys_process_vm_readv)
CALL(sys_process_vm_writev)
CALL(sys_foo)
1.3 添加系统调用号
gedit arch/arm/include/asm/unistd.h
在系统调用号为377后面添加,如下蓝色部分
#define __NR_setns (__NR_SYSCALL_BASE+375)
#define __NR_process_vm_readv (__NR_SYSCALL_BASE+376)
#define __NR_process_vm_writev (__NR_SYSCALL_BASE+377)
#define __NR_foo (__NR_SYSCALL_BASE+378)
1.5 添加系统调用函数的声明到头文件中
gedit include/linux/syscalls.h
在文件的最后添加如下蓝色部分;
asmlinkage long sys_setns(intfd, intnstype);
asmlinkage long sys_process_vm_readv(pid_tpid,
conststructiovec __user *lvec,
unsigned long liovcnt,
conststructiovec __user *rvec,
unsigned long riovcnt,
unsigned long flags);
asmlinkage long sys_process_vm_writev(pid_tpid,
conststructiovec __user *lvec,
unsigned long liovcnt,
conststructiovec __user *rvec,
unsigned long riovcnt,
unsigned long flags);
asmlinkage long sys_kcmp(pid_t pid1, pid_t pid2, int type,
unsigned long idx1, unsigned long idx2);
asmlinkage long sys_foo(void);
#endif
1.6 添加完毕后,重新编译内核,并且使用重新编译的内核来启动开发板。

什么是内核模块?
内核模块是一种没有经过链接,不能独立运行的目标文件,是在内核空间中运行的
程序。

经过链接装载到内核里面成为内核的一部分,可以访问内核的公用符号(函
数和变量)。

内核模块可以让操作系统内核在需要时载入和执行,在不需要时由操作系统卸载。

它们扩展了操作系统内核的功能却不需要重新启动系统。

模块“初始化”和“卸载”函数用什么来声明:
#include //包含的头文件
#include……..
#include……..
static int __initxxx_init(void) //xxx_init是模块入口函数名,自己定义
{
........
return 0;
}
static void __exit xxx_exit(void)
{
........
}
module_init(.......);
module_exit(.......);
MODULE_LICENSE(......);
模块加载与卸载:
加载模块最简单的方法是通过insmod命令。

insmod module
需要载入的模块名称由参数module指定,比如:
insmodhello_module.ko
类似地,卸载一个模块,你可使用rmmod命令。

rmmod module
比如,rmmodhello_module
列出已经挂载的内核模块命令:lsmod
Linux下设备的属性:
设备的类型:字符设备、块设备、网络设备;
主设备号:标识设备对应的驱动程序。

一般“一个主设备号对应一个驱动程序”
次设备号:每个驱动程序负责管理它所驱动的几个硬件实例,这些硬件实例则由次设备号来表示。

同一驱动下的实例编号,用于确定设备文件所指的设备。

文件名:设备结点的名字。

查看系统分配的设备号
#cat /proc/devices
手动添加设备文件(设备节点)
# mknod /dev/first_drv c 主设备号次设备号
设备号:
在内核驱动程序中,使用dev_t类型用于保存设备号。

dev_t是无符号32位整型;
高12位表示主设备号;
低20位是次设备号;
在驱动程序中,常常要通过设备号获取主、次设备号:
MAJOR(dev_tdev); //获取主设备号
MINOR(dev_tdev); //获取次设备号
通过主、次设备号合成设备号:
MKDEV(int major, int minor);
分配和释放字符设备号:
向内核申请已知的设备号:
intregister_chrdev_region(dev_t first, unsigned count,const char
*name);
first是要分配的起始设备号值。

count 所请求的连续设备编号的个数。

name设备名称,指和该编号范围建立关系的设备。

分配成功返回0,失败返回负数。

使用下面的函数来动态申请设备号:
intalloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned
count,const char *name);
dev是一个仅用于输出的参数, 它在函数成功完成时保存已分配范围
的第一个编号。

baseminor应当是请求的第一个要用的次设备号,它常常是0.
count 和name 参数跟request_chrdev_region的一样.
释放字符设备号:
不再使用时,释放这些设备编号。

使用以下函数:
void unregister_chrdev_region(dev_t from, unsigned count);
在模块的卸载函数中调用该函数。

大部分驱动程序涉及三个重要的内核数据结构:
文件操作file_operations结构体:
用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。

该结构体是底层操作函数的集合。

file_operations重要的成员:
标准C的标记化结构体的初始化方法:
structfile_operations fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release
};
文件对象file结构体:
文件对象file代表着一个打开的文件。

进程通过文件描述符fd与已打开文件
的file结构相联系。

指向结构体struct file的指针通常命名为filp。

索引节点inode结构体:
inode结构包含大量描述文件信息的成员变量。

但是对于描述设备文件的inode,跟设备驱动有关的成员只有两个。

dev_ti_rdev; 包含真正的设备号。

structfile_operations *i_fop;在生成设备文件的时候,这个文件操作成
员被赋予一个默认值;
从inode中获得主设备号和次设备号的宏:
unsignedintiminor(structinode *inode);
unsignedintimajor(structinode *inode);
file、inode、file_operations之间的关系:
◆file 表示打开的文件
◆inode 表示保存在磁盘上的文件
◆f ile_operations 表示硬件操作函数的集合
注册一个独立的cdev设备的基本过程如下:
(新方法)
为structcdev分配空间
structcdev *my_cdev = cdev_alloc();
初始化structcdev
voidcdev_init(structcdev *cdev, conststructfile_operations *fops) cdev设置完成,通知内核structcdev的信息(在执行这步之前必须确定
你对structcdev的以上设置已经完成!)
intcdev_add(structcdev *p, dev_tdev, unsigned count);
卸载字符设备时,调用相反的动作函数:
voidcdev_del(structcdev *dev);
(旧方法)
intregister_chrdev(unsigned intmajor, const char *name,
structfile_operations *fops);
major 是给定的主设备号。

name 是驱动的名字(将出现在/proc/devices),
fops 是设备驱动的file_operations结构。

register_chrdev将给设备分配0 - 255 的次设备号, 并且为每一个建立一
个缺省的cdev结构。

从系统中卸载字符设备的函数:
intunregister_chrdev(unsigned int major, const char *name);
字符设备驱动框架:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
//(1)编写硬件底层操作函数实现open,release,write,read
......
//(2)创建一个file_operations结构
......
static int __initxxx_init(void)
{
//(3)申请设备号
........
//(4)初始化cdev结构
........
//(5)注册cdev结构
........
return 0;
}
static void __exit XXX_exit(void)
{
//注销cdev结构
........
//注销设备号
}
module_init(.......); module_exit(.......);
MODULE_LICENSE(......);
编写字符设备驱动程序的流程:
实现底层操作函数(实现open,release,write,read )
将这些底层操作函数结合到file_operations 结构中; 在模块的入口函数
申请、注册设备号;
初始化cdev(要关联一个file_operations 结构) 注册cdev ; 在模块的出口函数
注销cdev ; 注销设备号;
操作硬件:
内核里面,通过一个宏来建立实地址到虚地址的映射,这个宏是:
ioremap(paddr,size);
宏的第一个参数是要映射的实地址; 第二个参数是要映射的大小; 宏返回映射后的虚拟地址;
例如,GPM4CON 控制寄存的地址映射如下:
unsigned long viraddr;
viraddr = ioremap(0x110002e0,8 );
通过 *(volatile unsigned long *)viraddr 就可以在内核中访问GPM4CON 寄存器。

取消映射的宏是:iounmap(viraddr);
底层操作函数,操作硬件
应用层用fd 表示打开的文件,fd 与内核的file 结构关联
每一个文件对应一个inode 结构
设备文件的自动建立:
class_create(owner, name);
创建一个类,就是在/sys/class/目录下创建一个目录;
struct device *device_create(struct class *cls,struct device *parent,dev_tdevt, void *drvdata,const char *fmt, ...);
fmt指明类目录下的目录,就是设备文件名;
对应的清理函数:
device_destroy(struct class *class, dev_tdevt)
class_destroy(struct class *cls)
如:#define FIRST_DRV_NAME "led_drv"
structclass *bb;
bb=class_create(THIS_MODULE, FIRST_DRV_NAME);
device_create(bb,NULL,dev,NULL,"led");
原子操作:两组原子操作:
整数原子操作:原子整数方法使用一种特殊的类型: atomic_t。

ATOMIC_INlT(inti);在声明一个atomic_t变量时,将
它初始化为i。

(int led_atomic =ATOMIC_INIT(1);)
原子整数操作最常见的用途就是实现计数器。

atomic_inc(atomic_t *v) 自加1
atomic_dec(atomic_t *v) 自减1
还可以用原子整数操作原子地执行一个操作并检查
结果。

一个常见的例子就是原子的减操作和检查。

intatomic_dec_and_test(atomic_t *v);
这个函数将给定的原子变量减1,如果结果
为0,就返回真;否则返回假。

位原子操作:voidset_bit(intnr,void *addr)
原子地设置addr所指对象的第nr位
void clear_bit(int nr,void*addr)
原子地清空addr所指对象的第nr位
void change_bit(int nr,void *addr)
原子地翻转addr所指对象的第nr位
自旋锁:自旋锁被一个线程持有时,其他线程不能获得这个锁,只能忙等这个锁,
自旋锁的使用:
DEFINE_SPINLOCK(mr_lock);
spin_lock (&mr_lock);
//临界区
spin_unlock (&mr_lock);
读写自旋锁:多个读者能够同时持有读锁.没有读者时只有一个写者能够持有写锁
DEFINE_RWLOCK(led_rwlock);
write_lock(&led_rwlock);
{…..}
write_unlock(&led_rwlock);
read_lock(&led_rwlock);
{…..}
read_unlock(&led_rwlock);
信号量:静态地声明信号量
DEFINE_SEMAPHORE(name)
name是信号量的名字,初始值是1;
也可以用下面方式来动态声明、初始化信号量
struct semaphore sem;
sema_init(&sem,count);
count是信号量的初值
down(struct semaphore *)//试图获得信号量,如果不能得到则进入不可中断睡眠
up(struct semaphore *)//释放给定的信号量,如果有等待的进程则唤醒。

读写信号量:互斥创建和初始化读写信号量:
静态声明读写信号量:
DECLARE_RWSEM(name) ;
初始化动态创建的读写信号量:
init_rwsem(structrw_semaphore *sem);
操作函数
down_read(structrw_semaphore *sem)、up_read、
down_write、up_write。

互斥体:可以认为是“初值为1的信号量”
其操作如下:
初始化:
DEFINE_MUTEX(mutexname);
mutex_init(mutex);
上锁:
mutex_lock(lock);
释放:
mutex_unlock(lock);
完成变量:类似于信号量
一个任务在等待完成变量,另一个进程在进行某种工作
另一个进程完成工作后,使用完成变量唤醒等待的进程
定义:
DECLARE_COMPLETION(work);
init_completion(struct completion *)
等待完成条件:
wait_for_completion(struct completion *)
通知等待的进程唤醒:
complete(struct completion *)
complete_all(struct completion *);
大内核锁:lock_kernel();
/* 临界区*/
unlock_kernel();
中断:
中断上下文:上半部(top half):
中断处理程序是上半部:接收到一个中断,它就立即开始执
行,但只做有严格时限的工作,例如对接收的中断进行应答
或复位硬件,这些工作都是在所有中断被禁止的情况下完成
的。

下半部(bottom half):
能够被允许稍后完成的工作会推迟到下半部(bottom half)去。

此后,在合适的时机,下半部会被开中断执行。

重入问题: Linux中的中断处理程序是无需重入的。

当一个给定的中断处理程序正在执行时,相应的中断线在所有处理
器上都会被屏蔽掉。

通常情况下,所有其他的中断都是打开的,所以这些不同中断线上
的其他中断都能被处理,但当前中断线总是被禁止的。

由此可以看出,同一个中断处理程序绝对不会被同时调用以处理嵌
套的中断。

注册、释放中断线:
request_irq()成功执行会返回0。

如果返回非0值,就表示有错误发生;
卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。

void free_irq(unsigned intirq,void* dev );
EXYNOS4_GPX3(2)是引脚的GPIO编号,在mach/gpio.h中定义;
gpio_to_irq会根据GPIO编号返回对应的中断号.
request_irq能够把中断号对应的中断处理函数注册到内核,并设置好中断后,开启对应的中断;
等待队列:
DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头部
的“快捷方式”(static DECLARE_WAIT_QUEUE_HEAD(btn_wp);)
等待事件:
wait_event(queue, condition);
等待第1个参数queue作为等待队列头部的队列被唤醒
第2个参数condition为真时唤醒,否则继续阻塞。

唤醒队列:
voidwake_up(wait_queue_head_t *queue);
voidwake_up_interruptible(wait_queue_head_t *queue);
上述操作会唤醒以queue作为等待队列头部的队列中所有的进程。

下半部实现的机制有哪些: softirqs, tasklet, 和work queue。

Jiffies:
全局变量jiffies用来记录自系统启动以来产生的节拍的总数。

启动时,内核将该变量初始化为0.
此后,每次时钟中断的处理程序会增加该变量的值。

因为一秒内时钟中断次数为HZ,所以jiffies一秒内增加的值为HZ。

定时器使用timer_list结构表示:
定义在<linux/timer.h>文件中
structtimer_list结构比较重要的成员如下:
expires :定时值
data: 传给定时器处理函数的参数
function :定时器处理函数
定时值的计算:
如当前的节拍数是jiffies
1秒后的定时值是jiffies+HZ
10ms后的定时值是jiffies+HZ/100
使用定时器,先要创建一个structtimer_list结构变量。

然后初始化该结构体变量的各个成员。

structtimer_listmytimer;
init_timer(&mytimer);
mytimer.expires = jiffies + 5*HZ;
mytimer.data = (unsigned long) dev;
mytimer.function = &my_timer_func;
激活定时器:
add_timer(structtimer_list * )
修改定时值并激活定时器:
mod_timer(structtimer_list *timer, unsigned long expires)
删除定时器:
del_timer(structtimer_list * )
删除定时器意味着定时器不再处于激活运行状态,可以用上面的两个激活函数重新激活运行。

使用定时器的流程如下:
先定义一个定时器处理函数,函数的原型如下:
voidmy_timer_function(unsigned long data);
创建定时器:
structtimer_listmy_timer;
设置定时器的值:
my_timer.expires = jiffies + HZ/100; //定时10ms
my_timer.data = 0;
my_timer.function = my_timer_function;
在需要是,激活定时器:
激活定时器:add_timer(&my_timer);
修改定时值,并激活定时器:mod_timer(&my_timer,jiffies+HZ/100);
void *kmalloc(size, gfp_mask);
用它可以获得以字节为单位的一块内核内存。

这个函数返回一个指向内存块的指针,其内存块至少要有size大小。

所分配的内存区在物理上是连续的。

在出错时,它返回NULL。

voidkfree(const void *ptr);
kfree()函数释放由kmalloc()分配出来的内存块。

例如: char *buf;
buf = kmalloc(BUF_SZ, GFP_KERNEL);
if (buf == NULL)
/* deal with error */
Else /* Do something with buf */
kfree(buf);
void *vmalloc(unsigned long size);
vmalloc()分配的内存虚拟地址是连续的,而物理地址则无需连续。

该函数返回一
个指针,指向逻辑上连续的一块内存区,其大小至少为size。

在发生错误时,函
数返回NULL。

voidvfree(void *addr);
kmalloc()与vmalloc()比较:
(1)kmalloc分配的内存虚拟地址连续,物理地址也连续;vmalloc分配的虚拟地址是连续的,但是物理地址不一定连续。

如果想给DMA分内存,
只能用kmalloc(size, GFP_DMA)
(2)kmalloc分配的内存不能超过128KB,vmalloc分配的内存是没有限制(3)kmalloc是可以使用在中断上下文中(原子过程),但要使用GFP_ATOMIC 分配标志kmalloc(size, GFP_ATOMIC),vmalloc是不可以使用在中断上下
文中的。

相关文档
最新文档