内核复习.docx
《内核复习.docx》由会员分享,可在线阅读,更多相关《内核复习.docx(28页珍藏版)》请在冰豆网上搜索。
内核复习
一、
1、
操作系统:
指在整个系统中负责完成最基本功能和系统管理的那些部分。
这些部分应该包括内核、设备驱动程序、启动引导程序、命令行shell或者其他种类的用户界面、基本的文件管理工具和系统工具。
2、
内核:
①内核是操作系统的内在核心。
系统其他部分必须依靠内核这部分软件提供的服务。
像管理硬件设备,分配系统资源等等。
内核有时候被称作是超级管理者或者是操作系统核心。
②通常一个内核由负责响应中断的中断服务程序,负责管理多个进程从而分享处理器时间的调度程序,负责管理进程地址空间的内存管理程序和网络、进程间通信等系统服务程序共同组成。
3
内核空间:
对于提供保护机制的现代系统来说,内核独立于普通应用程序,它一般处于系统态,拥有受保护的内存空间和访问硬件设备的所有权限。
这种系统态和被保护起来的内存空间,统称为内核空间。
用户空间:
相对的,应用程序在用户空间执行。
它们只能看到允许它们使用的部分系统资源,并且不能使用某些特定的系统功能,不能直接访问硬件,还有其他一些使用限制。
当内核运行的时候,系统以内核态进入内核空间,相反,普通用户程序以用户态进入用户空间。
4、
进程上下文:
当一个应用程序请求执行一条系统调用,我们说内核正在代其执行。
进一步解释,应用程序被称为通过系统调用在内核空间运行,而内核被称为运行于进程上下文中。
中断上下文:
①硬件设备想和系统通信的时候,它首先要发出一个异步的中断信号去打断内核正在执行的工作。
②中断通常对应着一个中断号,内核通过这个中断号查找相应的中断服务程序,并调用这个程序响应和处理中断。
③许多操作系统的中断服务程序都不在进程上下文中执行。
它们在一个与所有进程都无关的、专门的中断上下文中运行。
5、
上下文代表着内核活动的范围。
实际上我们可以将处理器在任何指定时间点上的活动范围概括为下列三者之一:
运行于内核空间,处于进程上下文,代表某个特定的进程执行。
运行干内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断。
运行于用户空间,执行用户进程。
6、
单内核与微内核设计之比较:
单内核(Linux、Unix):
所谓单内核就是把它从整体上作为一个单独的大过程来实现,并同时运行在一个单独的地址空间。
因此,这样的内核通常以单个静态二进制文件的形式存放于磁盘。
所有内核服务都在这样的一个大内核空间中运行。
内核各个模块可以直接调用函数来进行通信。
微内核(WindowNT):
微内核的功能被划分为独立的过程,每个过程叫做一个服务器。
所有的服务器都保持独立并运行在各自的地址空间。
服务器的各自独立有效地避免了一个服务器的失效祸及另一个。
服务器间的通信需要通过消息传递来实现。
二、
1、
相对于用户空间内应用程序的开发,内核开发有很大的不同。
最重要的差异包括以下几种:
①内核编程时不能访问C库。
②内核编程时必须使用GNUC。
③内核编程时缺乏像用户空间那样的内存保护机制。
④内核编程时浮点数很难使用。
⑤内核只有一个很小的定长堆栈。
⑥由于内核支持异步中断、抢占和SMP,因此必须时刻注意同步和并发。
⑦要考虑可移植性的重要性。
2、
①与用户空间的应用程序不同,内核不能链接使用标准C函数库,对内核来说,完整的c库太大了,但是,大部分常用的C库函数在内核中都已经得到了实现。
②在内核编程中,只能使用内核源代码树中的头文件。
③内核代码虽然无法调用printf(),但它可以调用printk()函数。
3、
内联(inline)函数:
定义一个内联函数的时候,需要使用static作为关键字,并且用inline限定它。
比如:
staticinlinevoiddog(unignedlongtail_size){}
定义:
内联函数必须在使用之前就定义好,否则编译器就没法把这个函数展开。
实践中一般在头文件中定义内联函数。
由于使用了static作为关键字进行限制,所以编译时不会为内联函数单独建立一个函数体。
如果一个内联函数仅仅在某个源文件中使用,那么也可以把它定义在该文件开始的地方。
4、
没有内存保护机制:
①如果一个用户程序试图进行一次非法的内存访问,内核会发现这个错误,发送SIGSEGV,并结束整个进程。
然而,如果是内核自己非法访问了内存,那后果就很难控制了。
内核中发生的内存错误会导致oops,这是内核中出现的最常见的一类错误。
在内核中,不应该去做访问非法的内存地址,引用空指针之类的事情,否则它可能会死掉,却根本不知会你一声——在内核里,风险常常会比外面大一些。
②此外,内核中的内存都不分页。
也就是说,每用掉一个字节,物理内存就减少一个字节。
所以,在你想往内核里加入什么新功能的时候,要记住这一点。
5、
不要轻易在内核中使用浮点数:
①在用户空间的进程内进行浮点操作的时候,内核会完成从整数操作到浮点数操作的模式转换。
在执行浮点指令时到底会做些什么,因体系结构不同,内核的选择也不同,但是,内核通常捕获陷阱并做相应处理。
②和用户空间进程不同,内核并不能完美地支持浮点操作,因为它本身不能陷入。
在内核中使用浮点数时,除了要人工保存和恢复浮点寄存器,还有其他一些琐碎的事情要做。
如果要直截了当的回答,那就是,别这么做了,不要在内核中使用浮点数。
6、
容积小而固定的栈:
①用户空间的程序可以从栈上分配大量的空间来存放变量,甚至巨大的结构体或者是包含许多数据项的数组都没有问题。
之所以可以这么做,是因为用户空间的栈本身比较大,而且还能动态的增长。
②内核栈的准确大小随体系结构而变。
在x86上,栈的大小在编译时配置,可以是4KB也可以是8KB。
从历史上说,内核栈的大小是两页,这就意味着,32位机的内核栈是8KB,而64位机是16KB,这是固定不变的。
每个处理器都有自己的栈。
7、
同步和并发:
①内核很容易产生竞争条件。
②Linux是抢占多任务操作系统。
③Linux内核支持多处理器系统。
④中断是异步到来的,完全不顾及当前正在执行的代码。
⑤Linux内核可以抢占。
常用的解决竞争的办法是自旋锁和信号量。
8、
可移植性的重要性:
Linux是一个可移植的操作系统,并且一直保持这种特点。
也就是说,大部分C代码应该与体系结构无关,在许多不同体系结构的计算机上都能够编译和执行,因此,必须把体系结构相关的代码从内核代码树的特定目录中适当地分离出来。
三、
1、
嵌入式linux系统的组成包括三部分:
bootloader程序,linux内核,根文件系统。
①Bootloader程序:
系统上电时第一个要执行的程序,主要功能是为启动linux内核准备环境;
②Linux内核:
操作系统的核心部分,是一个单一的文件(uImage);
③根文件系统:
根文件系统包括操作系统的一些系统工具,C函数库等的东西。
2、
嵌入式linux软件开发的特点:
①需要交叉编译:
1、硬件平台处理器较慢,内存和外存容量小等等;
2、把软件开发放在高性能的PC机上进行;
3、PC机上CPU指令集与嵌入式CPU的指令集是不同的,因此,在PC机上开发嵌入式软件需要交叉编译。
②需要移植:
1、由于嵌入式系统是一个软硬件定制的系统,硬件平台各不相同,软件设计需要根据不同的硬件设备来添加或修改相应的代码,这就是移植。
3、
交叉开发环境是指编译、链接和调试嵌入式应用软件的环境,它与运行嵌入式应用软件的环境有所不同,通常采用宿主机-目标机模式。
4、
5、
嵌入式软件开发过程中的交叉调试与本地软件开发过程中的调试方式有所差别:
本地软件开发:
①调试器与被调试的程序往往运行在同一台计算机上。
②调试器是一个单独运行着的进程,它通过操作系统提供的调试接口来控制被调试的进程。
嵌入式软件开发:
①调试时采用的是在宿主机和目标机之间进行的交叉调试。
②调试器运行在宿主机,但被调试的进程却是运行在目标板。
③调试器和被调试进程通过串口或者网络进行通信,调试器可以控制、访问被调试进程,读取被调试进程的当前状态,并能够改变被调试进程的运行状态。
六、
1、
一个API定义了一组应用程序使用的编程接口,应用编程接口(API)与系统调用的关系:
①应用程序编程接口实际上并不需要和内核提供的系统调用对应。
②一个API可以实现成一个系统调用;
③一个API也可以通过调用多个系统调用来实现
④一个API也可以完全不使用任何系统调用。
实际上,API可以在各种不同的操作系统上实现,给应用程序提供完全相同的接口。
2、
系统调用(在Linux中常称作syscalls):
通常通过函数进行调用。
①它们通常都需要定义一个或几个参数。
②系统调用还会通过一个long类型的返回值来表示成功或者错误。
③通常,但也不绝对,用一个负的返回值来表明错误。
④返回一个0值通常(当然仍不是绝对的)表明成功。
Unix系统调用在出现错误的时候会把错误码写入errno全局变量。
通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串。
3、
系统调用号:
①每个系统调用被赋予一个系统调用号。
②系统调用号关联系统调用。
当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用;进程不会提及系统调用的名称。
③系统调用号相当关键,一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。
④此外,如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用,否则,以前编译过的代码会调用这个系统调用。
4、
系统调用表(sys_call_table):
①表中存放每个的系统调用的入口地址(函数指针);
②每一个体系结构都有对应的一个系统调用表;
5、
系统调用的处理过程:
①用户空间的程序无法直接执行内核代码。
②所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
③通知内核的机制是靠软中断实现的:
通过引发一个异常来促使系统切换到内核态去执行异常处理程序。
6、
系统调用的实现:
1)参数验证:
①系统调用必须仔细检查它们所有的参数是否合法有效。
②系统调用在内核空间执行,如果任由用户将不合法的输入传递给内核,那么系统的安全和稳定将面临极大的考验。
③举例来说,与文件I/O相关的系统调用必须检查文件描述符是否有效。
与进程相关的函数必须检查提供的PID是否有效。
必须检查每个参数,保证它们不但合法有效,而且正确。
2)检查用户提供的指针是否有效。
在接收一个用户空间的指针之前,内核必须保证:
①指针指向的内存区域属于用户空间。
进程决不能哄骗内核去读内核空间的数据。
②指针指向的内存区域在进程的地址空间里。
进程决不能哄骗内核去读其他进程的数据。
③如果是读,该内存应被标记为可读。
如果是写,该内存应被标记为可写。
进程决不能绕过内存访问限制。
3)最后一项检查针对是否有合法权限:
①新的系统允许检查针对特定资源的特殊权限。
调用者可以使用capable()函数来检查是否有权能对指定的资源进行操作,如果它返回非零值,调用者就有权进行操作,返回零则无权操作。
②举个例子,capable(CAP_SYS_NICE)可以检查调用者是否有权改变其他进程的nice值。
默认情况下,属于超级用户的进程拥有所有权利而非超级用户没有任何权利。
7、
为什么不通过系统调用的方式实现:
建立一个新的系统调用的好处:
①系统调用创建容易且使用方便。
②Linux系统调用的高性能显而易见。
问题:
①你需要一个系统调用号,而这需要在一个内核在处于开发版本的时候由官方分配给你。
②系统调用被加入稳定内核后就被固化了,为了避免应用程序的崩溃,它的界面不允许做改动。
③需要将系统调用分别注册到每个需要支持的体系结构中去。
④在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用。
⑤如果仅仅进行简单的信息交换,系统调用就大材小用了。
替代方法:
①创建一个设备节点,通过read()和、write()访问它。
用ioctl()进行特别的设置操作和获取特别信息。
②一些接口如信号量,可以用文件描述符表示以进行操作。
像信号量这样的某些接口,可以用文件描述符来表示,因此也就可以按上述方式对其进行操作。
③把增加的信息作为一个文件放在sysfs的合适位置。
七、
1、
什么是内核模块?
①定义:
内核模块是一种没有经过链接,不能独立运行的目标文件,是在内核空间中运行的程序。
经过链接装载到内核里面成为内核的一部分,可以访问内核的公用符号(函数和变量)。
②内核模块可以让操作系统内核在需要时载入和执行,在不需要时由操作系统卸载。
它们扩展了操作系统内核的功能却不需要重新启动系统。
③如果没有内核模块,我们不得不一次又一次重新编译生成单内核操作系统的内核镜像来加入新的功能。
这还意味着一个臃肿的内核。
2、
模块机制的优点:
①减小内核映像尺寸,增加系统灵活性;
②节省开发时间;修改内核,不必重新编译整个内核。
③模块的目标代码一旦被链入内核,作用和静态链接的内核目标代码完全等价。
模块机制的缺点:
①对系统性能有一定损失;
②使用不当时会导致系统崩溃;
3、
一个内核模块至少包含两个函数:
①初始化函数,在模块加载到内核时被调用。
②卸载函数,在内核模块被卸载时被调用。
③现在的模块“初始化”和“卸载”函数可以起任意的名字。
通过宏module_init()声明初始化函数,通过module_exit()声明卸载函数。
④任一个内核模块需要包含linux/module.h
4、
关于__init和__exit宏:
①如果该模块被编译进内核,而不是动态加载,则宏__init的使用会在初始化完成后丢弃该函数并收回所占内存。
②如果该模块被编译进内核,宏__exit将忽略“清理收尾”的函数。
③这些宏在头文件linux/init.h定义。
5、
内核模块证书和内核模块文档说明:
①2.4内核后,引入识别代码是否在GPL许可下发布的机制。
在使用非公开的源代码产品时会得到警告。
通过宏MODULE_LICENSE(“GPL”),设置模块遵守GPL证书,取消警告信息。
②宏MODULE_DESCRIPTION()用来描述模块的用途。
③宏MODULE_AUTHOR()用来声明模块的作者。
④这些宏都在头文件linux/module.h定义。
使用这些宏只是用来提供识别信息。
6、
编译模块过程中的第一步是决定在那里管理模块源码:
①你可以把模块源码加入到内核源代码树中;
②也可以在内核源代码树之外维护和构建你的模块源码。
八、
1、
Linux设备驱动概述:
1)操作系统内核是通过各种驱动程序来驾驭硬件设备,它为用户屏蔽了各种各样的设备。
2)设备驱动程序是操作系统内核和机器硬件之间的接口,系统调用是操作系统内核和应用程序之间的接口。
3)在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作.
2、
驱动完成的功能:
1)对设备初始化和释放.
2)把数据从内核传送到硬件和从硬件读取数据.
3)读取应用程序传送给设备文件的数据和回送应用程序请求的数据.
4)检测和处理设备出现的错误.
3、
Linux下设备的属性:
1)设备的类型:
字符设备、块设备、网络设备;
2)主设备号:
标识设备对应的驱动程序。
一般“一个主设备号对应一个驱动程序”
3)次设备号:
每个驱动程序负责管理它所驱动的几个硬件实例,这些硬件实例则由次设备号来表示。
同一驱动下的实例编号,用于确定设备文件所指的设备。
4)文件名:
设备结点的名字。
4、
设备号:
①在内核驱动程序中,使用dev_t类型用于保存设备号。
dev_t是无符号32位整型;
高12位表示主设备号;
低20位是次设备号;
②在驱动程序中,常常要通过设备号获取主、次设备号:
MAJOR(dev_tdev);//获取主设备号
MINOR(dev_tdev);//获取次设备号
③通过主、次设备号合成设备号:
MKDEV(intmajor,intminor);
④dev_t类型以后可能会发生变化,但只要使用这些宏,就可保证设备驱动程序的正确性。
5、
①编写驱动程序要做的第一件事,为字符设备获取一个设备号。
1)如果事先知道所需要的设备号(主设备号)的情况,可以使用以下函数,向内核申请已知的设备号:
intregister_chrdev_region(dev_tfirst,unsignedcount,constchar*name);
first是要分配的起始设备号值。
count所请求的连续设备编号的个数。
name设备名称,指和该编号范围建立关系的设备。
分配成功返回0,失败返回负数。
2)可以让内核动态分配设备号(主要是主设备号);使用下面的函数来动态申请设备号;
intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name);
dev是一个仅用于输出的参数,它在函数成功完成时保存已分配范围的第一个编号。
baseminor应当是请求的第一个要用的次设备号,它常常是0.
count和name参数跟request_chrdev_region的一样.
3)不再使用时,释放这些设备编号。
使用以下函数:
voidunregister_chrdev_region(dev_tfrom,unsignedcount);
在模块的卸载函数中调用该函数。
②编写设备驱动的第二步就是注册该设备
①为structcdev分配空间:
structcdev*my_cdev=cdev_alloc();
②初始化structcdev:
voidcdev_init(structcdev*cdev,conststructfile_operations*fops)
③cdev设置完成,通知内核structcdev的信息(在执行这步之前必须确定你对structcdev的以上设置已经完成!
):
intcdev_add(structcdev*p,dev_tdev,unsignedcount);
dev是要添加的设备的cdev结构,
num是这个设备对应的第一个设备号,
count是应当关联到设备的设备号的数目.
④卸载字符设备:
voidcdev_del(structcdev*dev);
早期方法:
①注册一个字符设备的早期方法:
intregister_chrdev(unsignedintmajor,constchar*name,structfile_operations*fops);
major是给定的主设备号。
为0代表什么?
name是驱动的名字(将出现在/proc/devices),
fops是设备驱动的file_operations结构。
register_chrdev将给设备分配0-255的次设备号,并且为每一个建立一个缺省的cdev结构。
②从系统中卸载字符设备的函数:
intunregister_chrdev(unsignedintmajor,constchar*name);
6、
大部分驱动程序涉及三个重要的内核数据结构:
①文件操作file_operations结构体
②文件对象file结构体
③索引节点inode结构体
1)file_operations重要的成员
owner:
指向拥有该结构体的模块的指针,内核使用该指针维护模块使用计数。
llseek:
用来修改文件的当前读写位置,把新位置作为返回值返回,loff_t是在LINUX中定义的长偏移量.
read:
用来从设备中读取数据。
非负返回值表示成功读取的直接数。
write:
向设备发送数据。
ioctl:
提供一种执行设备特定命令的方法。
2)索引节点inode结构
文件打开,在内存建立副本后,由唯一的索引节点inode描述。
与file结构不同。
①file结构是进程使用的结构,进程每打开一个文件,就建立一个file结构。
不同的进程打开同一个文件,建立不同的file结构。
②inode结构是内核使用的结构,文件在内存建立副本,就建立一个inode结构来描述。
一个文件在内存里面只有一个inode结构对应。
3)
①structinode结构代表一个实实在在文件,每个文件只对应一个inode;
②structfile结构代表一个打开的文件,同一个文件可以对应多个file结构;
③structfile_operations结构代表底层操作硬件函数的集合;
九、
1、
应用层与驱动层层次结构框图:
2、
注册cdev分析:
cdev结构的注册分两步,第一步是初始化cdev结构;第二步是注册。
cdev_add()函数分析如下:
3、
字符设备注册过程如下图:
内核里面,通过一个宏来建立实地址到虚地址的映射,这个宏是:
ioremap(paddr,size);
宏的第一个参数是要映射的实地址;
第二个参数是要映射的大小;
宏返回映射后的虚拟地址;
例如,GPM4CON控制寄存的地址映射如下:
unsignedlongviraddr;
viraddr=ioremap(0x110002e0,8);
通过*(volatileunsignedlong*)viraddr就可以在内核中访问GPM4CON寄存器。
取消映射的宏是:
iounmap(viraddr);
十、
并发执行的原因:
①在一段内核代码操作某资源的时候系统产生了一个中断,而且该中断的处理程序还要访问这一资源,这就是一个bug;
②一段内核代码在访问一个共享资源期间可以被抢占;
③内核代码在临界区里睡眠;
④最后还要注意,两个处理器绝对不能同时访问同一共享数据.
这里的基本原则是:
在编写代码的开始阶段就要设计恰当的锁。
十一、
1、
原子整数操作:
1)原子整数方法使用一种特殊的类型:
atomic_t
2)引入了一个特殊数据类型主要是出于一下原因:
①首先,让原子函数只接受atomic_t类型的操作数,可以确保原子操作函数只与这种特殊类型数据一起使用。
②同时,这也保证了该类型的数据不会被传递给其他任何非原子函数。
③最后,在不同体系结构上实现原子操作的时候,使用atomic_t可以屏蔽其间的差异。
2、
原子整数操作最常见的用途就是实现计数器:
atomic_inc(atomic_t*v)自加1
atomic_dec(atomic_t*v)自减1
②还可以用原子整数操作原子地执行一个操作并检查结果。
一个常见的例子就是原子的减操作和检查。
intatomic_dec_and_test(atomic_t*v);
这个函数将给定的原子变量减1,如果结果为0,就返回真;否则返回假。
十二、
1、
自旋锁:
1)任何时候只能有一个线程持有的锁
2)注意:
自旋锁被一个线程持有时,其他线程不能获得这个锁,只能忙等这个锁,
3)长时间占有自旋锁并不是一个明智的方法
4)接口定义在文件中
5)自旋锁的使用:
DEFINE_SPINLO