《Linux内核设计与实现》读书笔记(十九)- 可移植性
linux内核的移植性非常好, 目前的内核也支持非常多的体系结构(有20多个).
但是刚开始时, linux也只支持 intel i386 架构, 从 v1.2版开始支持 Digital Alpha, Intel x86, MIPS和SPARC(虽然支持的还不是很完善).
从 v2.0版本开始加入了对 Motorala 68K和PowerPC的官方支持, v2.2版本开始新增了 ARMS, IBM S390和UltraSPARC的支持.
v2.4版本支持的体系结构数达到了15个, v2.6版本支持的体系结构数目提高到了21个.
目前的我使用的系统是 Fedora20, 支持的体系结构有31个之多.(源码树中 arch目录下有支持的体系结构, 每种体系结构一个文件夹)
考虑到内核支持如此之多的架构, 在内核开发的时候就需要考虑编码的可移植性.
提高可移植性最重要的就是要搞明白不同体系结构之间究竟是什么对移植代码的影响比较大.
主要内容:
- 字长
- 数据类型
- 数据对齐
- 字节顺序
- 时间
- 页长度
- 处理器顺序
- SMP, 内核抢占, 高端内存
- 总结
1. 字长
这里的字是指处理器能够一次完成处理的数据. 字长即使处理器能够一次完成处理的数据的最大长度.
目前的处理器主要有32位和64为2种, 注意这里的32位和64位并不是指操作系统的版本, 而是指处理器的能力.
一般来说, 32位的处理器只能安装32位的操作系统, 而64位的处理器可以安装32位的操作系统, 也可以安装64位的操作系统.
对于一种体系结构来说, 处理器通用寄存器(general-purpose registers, GPR)的大小和它的字长是相同的.
C语言定义的long类型总是对等于机器的字长, 而int型有时会比字长小.
- 32位的体系结构中, int型和long型都是32位的
- 64位的体系结构中, int型是32位的, long型是64位的.
内核编码中涉及到字长的部分时, 牢记以下准则:
- ANSI C标准规定, 一个char的长度一定是一个字节(8位)
- linux当前所支持的体系结构中, int型都是32位的
- linux当前所支持的体系结构中, short型都是16位的
- linux当前所支持的体系结构中, 指针和long型的长度不定, 在32位和64位中变化
- 不能假设 sizeof(int) == sizeof(long)
- 类似的, 不能假定 指针的长度和int型相同.
此外, 操作系统有个简单的助记符来描述此系统中数据类型的大小.
- LLP64 :: 64位的Windows, long类型和指针都是64位
- LP64 :: 64位的Linux, long类型和指针都是64位
- ILP32 :: 32位的Linux, int类型, long类型和指针都是32位
- ILP64 :: int类型, long类型和指针都是64位(非Linux)
2. 数据类型
编写可移植性代码时, 内核中的数据类型有以下3点需要注意:
2.1 不透明类型
linux内核中定义了很多不透明类型, 它们是在C语言标准类型上的一个封装, 比如 pid_t, uid_t, gid_t 等等.
例如, pid_t的定义可以在源码中找到:
typedef __kernel_pid_t pid_t; /* include/linux/types.h */ typedef int __kernel_pid_t; /* arch/asm/include/asm/posix_types.h */
使用这些不透明类型时, 以下原则需要注意:
- 不要假设该类型的长度(那怕通过源码看到了它的C语言类型), 这些类型在不同体系结构中可能长度会变, 内核开发者也有可能修改它们
- 不要将这些不透明类型转换为C标准类型来使用
- 编程时保证不透明类型实际存储空间或者格式发生变化时代码不受影响
2.2 长度确定的类型
除了不透明类型, linux内核中还定义了一系列长度明确的数据类型, 参见 include/asm-generic/int-l64.h 或者 include/asm-generic/int-ll64.h
typedef signed char s8;
typedef unsigned char u8; typedef signed short s16;
typedef unsigned short u16; typedef signed int s32;
typedef unsigned int u32; typedef signed long s64;
typedef unsigned long u64;
上面这些类型只能在内核空间使用, 用户空间无法使用. 用户空间有对应的变量类型, 名称前多了2个下划线:
typedef __signed__ char __s8;
typedef unsigned char __u8; typedef __signed__ short __s16;
typedef unsigned short __u16; typedef __signed__ int __s32;
typedef unsigned int __u32; typedef __signed__ long __s64;
typedef unsigned long __u64;
2.3 char类型
之所以把char类型单独拿出来说明, 是因为char类型在不同的体系结构中, 有时默认是带符号的, 有时是不带符号的.
比如, 最简单的例子:
/*
* 某些体系结构中, char类型默认是带符号的, 那么下面 i 的值就为 -1
* 某些体系结构中, char类型默认是不带符号的, 那么下面 i 的值就为 255, 与预期可能有差别!!!
*/
char i = -;
避免上述问题的方法就是, 给char类型赋值时, 明确是否带符号, 如下:
signed char i = -; /* 明确 signed, i 的值在哪种体系结构中都是 -1 */
unsigned char i = ; /* 明确 unsigned, i 的值在哪种体系结构中都是 255 */
3. 数据对齐
数据对齐也是增强可移植性的一个重要方面(有的体系结构对数据对齐要求非常严格, 载入未对齐的数据可导致性能下降, 甚至错误).
数据对齐的意思就是: 数据的内存地址可以被 4 整除
1. 通过指针转换类型时, 不要转换长度不一样的类型, 比如下面的代码有可能出错
/*
* 下面的代码将一个变量从 char 类型转换为 unsigned long 类型,
* char 类型只占 1个字节, 它的地址不一定能被4整除, 转换为 4个字节或者8个字节的 usigned long之后,
* 导致 unsigned long 出现数据不对齐的现象.
*/
char wolf[] = "Like a wolf";
char *p = &wolf[];
unsigned long p1 = *(unsigned long*) p;
2. 对于数组, 安装基本数据类型进行对齐就行.(数组元素的存放在内存中是连续的, 第一个对齐了, 后面的都自动对齐了)
3. 对于联合体, 长度最大的数据对齐就可以了
4. 对于结构体, 保证结构体中每个元素能够正确对齐即可
如果结构体中的元素没有对齐, 编译器会自动填充结构体, 保证它是对齐的. 比如下面的代码, 预计应该输出12, 实际却输出了24
我的代码运行环境: Fedora20 x86_64
/******************************************************************************
* @file : struct_align.c
* @author : wangyubin
* @date : 2014-01-09
*
* @brief :
* history : init
******************************************************************************/ #include <stdio.h> struct animal_struct
{
char dog; /* 1个字节 */
unsigned long cat; /* 8个字节 */
unsigned short pig; /* 2个字节 */
char fox; /* 1个字节 */
}; int main(int argc, char *argv[])
{
/* 在我的64bit 系统中是按8位对齐, 下面的代码输出 24 */
printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct));
return ;
}
测试方法:
gcc -o test struct_align.c
./test # 输出24
结构体应该被填充成如下形式:
struct animal_struct
{
char dog; /* 1个字节 */
/* 此处填充了7个字节 */
unsigned long cat; /* 8个字节 */
unsigned short pig; /* 2个字节 */
char fox; /* 1个字节 */
/* 此处填充了5个字节 */
};
通过调整结构体中元素顺序, 可以减少填充的字节数, 比如上述结构体如果定义成如下顺序:
struct animal_struct
{
unsigned long cat; /* 8个字节 */
unsigned short pig; /* 2个字节 */
char dog; /* 1个字节 */
char fox; /* 1个字节 */
};
那么为了保证8位对齐, 只需在后面补充 4位即可:
struct animal_struct
{
unsigned long cat; /* 8个字节 */
unsigned short pig; /* 2个字节 */
char dog; /* 1个字节 */
char fox; /* 1个字节 */
/* 此处填充了4个字节 */
};
调整后的代码会输出 16, 不是之前的24
#include <stdio.h> struct animal_struct
{
unsigned long cat; /* 8个字节 */
unsigned short pig; /* 2个字节 */
char dog; /* 1个字节 */
char fox; /* 1个字节 */
}; int main(int argc, char *argv[])
{
/* 在我的64bit 系统中是按8位对齐, 下面的代码输出 16 */
printf ("sizeof(animal_struct)=%d\n", sizeof(struct animal_struct));
return ;
}
测试方法:
gcc -o test struct_align.c
./test # 输出16
注意: 虽然调整结构体中元素的顺序可以减少填充的字节, 从而降低内存的消耗.
但是对于内核中已有的那些结构, 千万不能随便调整其元素顺序, 因为内核中很多现存的方法都是通过元素在结构体中位置偏移来获取元素的.
4. 字节顺序
字节顺序其实只有2种:
- 低位优先 :: little-endian 数据由低位地址->高位地址存放
- 高位优先 :: big-endian 数据由高位地址->低位地址存放
比如占有四个字节的整数的二进制表示如下:
内存地址方向: 高位 <--------------------> 低位
little-endian 表示如下:
big-endian 表示如下:
判断一个体系结构是 big-endian 还是 little-endian 非常简单.
int x = ; /* 二进制 00000000 00000000 00000000 00000001 */ /*
* 内存地址方向: 高位 <--------------------> 低位
* little-endian 表示: 00000000 00000000 00000000 00000001
* big-endian 表示: 00000001 00000000 00000000 00000000
*/
if (*(char *) &x == ) /* 这句话把int型转为char型, 相当于只取了int型的最低8bit */
/* little-endian */
else
/* big-endian */
5. 时间
内核中使用到时间相关概念时, 为了提高可移植性, 不要使用时间中断的发生频率(也就是每秒产生的jiffies), 而应该使用 HZ 来正确使用时间.
关于 jiffies 和 HZ 的概念, 可以参考之前的博客: 《Linux内核设计与实现》读书笔记(十一)- 定时器和时间管理
6. 页长度
当处理用页管理的内存时, 不要既定页的长度为 4KB, 在不同的体系结构中长度会不一样.
而应该使用 PAGE_SIZE 以字节数来表示页长度, 使用 PAGE_SHIFT 表示从最右端屏蔽了多少位能够得到该地址对应的页的页号.
PAGE_SIZE 和 PAGE_SHIFT 都是宏, 定义在 include/asm-generic/page.h 中
下表是一些体系结构中页长度:
体系结构 |
PAGE_SHIFT |
PAGE_SIZE |
alpha | 13 | 8KB |
arm | 12, 14, 15 | 4KB, 16KB, 32KB |
avr | 12 | 4KB |
cris | 13 | 8KB |
blackfin | 12 | 16KB |
h8300 | 14 | 4KB |
12 | 4KB, 8KB, 16KB, 32KB | |
m32r | 12, 13, 14, 16 | 4KB |
m68k | 12 | 4KB, 8KB |
m68knommu | 12, 13 | 4KB |
mips | 12 | 4KB |
min10300 | 12 | 4KB |
parisc | 12 | 4KB |
powerpc | 12 | 4KB |
s390 | 12 | 4KB |
sh | 12 | 4KB |
sparc | 12, 13 | 4KB, 8KB |
um | 12 | 4KB |
x86 | 12 | 4KB |
xtensa | 12 | 4KB |
7. 处理器顺序
还有最后一个和可移植性相关的注意点就是处理器对代码的执行顺序, 在有些体系结构中, 处理器并不是严格按照代码编写的顺序执行的,
可能为了优化性能或者其他原因, 处理器执行指令的顺序与编写的代码的顺序稍有出入.
如果我们的某段代码需要严格的执行顺序, 需要在代码中使用 rmb() wmb() 等内存屏障来确保处理器的执行顺序.
关于rmb和wmb可以参考之前的博客: 《Linux内核设计与实现》读书笔记(十)- 内核同步方法 第 11 小节
8. SMP, 内核抢占, 高端内存
SMP, 内核抢占和高端内存本身虽然和可移植性没有太大的关系, 但它们都是内核中重要的配置选项,
如果编码时能够考虑到这些的话, 那么即使内核修改SMP等这些配置选项, 我们的代码仍然可以安全可靠的运行.
所以, 在编写内核代码时最好加上如下假设:
- 假设代码会在SMP系统上运行, 要正确选择和使用锁
- 假设代码会在支持内核抢占的情况下运行, 要正确使用锁和内核抢占语句
- 假设代码会运行在使用高端内存(非永久映射内存)的系统上, 必要时使用 kmap()
9. 总结
编写简洁, 可移植性的代码还需要通过实践来积累经验, 上面的准则可以作为代码是否满足可移植性的一些检测条件.
书中还提到的2点注意事项, 我觉得不仅是编写内核代码, 编写任何代码时, 都应该注意:
- 编码尽量选取最大公因子 :: 假定任何事情都有可能发生, 任何潜在的约束也都存在
- 编码尽量选取最小公约数 :: 不要假定给定的内核特性是可用的, 仅仅需要最小的体系结构功能
虽然编写可移植性代码需要遵守这么多的原则, 但是不能畏惧, 在学习内核开发的过程中, 只有不断的尝试, 不断的犯错, 才能确实的掌握内核.
《Linux内核设计与实现》读书笔记(十九)- 可移植性的更多相关文章
- Linux内核设计与实现 读书笔记 转
Linux内核设计与实现 读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://bl ...
- Linux内核设计与实现 读书笔记
第三章 进程管理 1. fork系统调用从内核返回两次: 一次返回到子进程,一次返回到父进程 2. task_struct结构是用slab分配器分配的,2.6以前的是放在内核栈的栈底的:所有进程的ta ...
- Linux内核设计与实现读书笔记(8)-内核同步方法【转】
转自:http://blog.chinaunix.net/uid-10469829-id-2953001.html 1.原子操作可以保证指令以原子的方式执行——执行过程不被打断.内核提供了两组原子操作 ...
- Linux内核设计与实现——读书笔记2:进程管理
1.进程: (1)处于执行期的程序,但不止是代码,还包括各种程序运行时所需的资源,实际上进程是正在执行的 程序的实时结果. (2)程序的本身并不是进程,进程是处于执行期的程序及其相关资源的总称. (3 ...
- Linux内核设计与实现——读书笔记1:内核简介
内核:有的时候被称管理者或者操作系统核心,通常内核负责响应中断的中断服务程序, 负责管理多个进程从而分享处理器时间的调度程序,负责管理进程地址空间德内存管理程序 和网络,进程间通信等系统服务程序共同组 ...
- 初探内核之《Linux内核设计与实现》笔记上
内核简介 本篇简单介绍内核相关的基本概念. 主要内容: 单内核和微内核 内核版本号 1. 单内核和微内核 原理 优势 劣势 单内核 整个内核都在一个大内核地址空间上运行. 1. 简单.2. 高效 ...
- 《Linux内核设计与实现》第十八章读书笔记
1.内核中的bug 内核中的bug表现得不像用户级程序中那么清晰——因为内核.用户以及硬件之间的交互会很微妙: 从隐藏在源代码中的错误到展现在目击者面前的bug,往往是经历一系列连锁反应的事件才可能触 ...
- Linux内核设计与实现 总结笔记(第十六章)页高速缓存和页回写
页高速缓存是Linux内核实现磁盘缓存.磁盘告诉缓存重要源自:第一,访问磁盘的速度要远远低于访问内存. 第二,数据一旦被访问,就很有可能在短期内再次被访问到.这种短时期内集中访问同一片数据的原理称作临 ...
- Linux内核架构与底层--读书笔记
linux中管道符"|"的作用 命令格式:命令A|命令B,即命令1的正确输出作为命令B的操作对象(下图应用别人的图片) 1. 例如: ps aux | grep "tes ...
- Linux内核设计与实现 总结笔记(第十章)内核同步方法
一.原子操作 原子操作可以保证指令以原子的方式执行----执行过程不被打断. 1.1 原子整数操作 针对整数的原子操作只能对atomic_t类型的数据进行处理. 首先,让原子函数只接收atomic_t ...
随机推荐
- JSON之Asp.net MVC C#对象转JSON,DataTable转JSON,List转JSON,JSON转List,JSON转C#对象
一.JSON解析与字符串化 JSON.stringify() 序列化对象.数组或原始值 语法:JSON.stringify(o,filter,indent) o,要转换成JSON的对象.数组或原始值 ...
- Android IOS WebRTC 音视频开发总结(七三)-- 我为什么走上了创业这条不归路?
本文主要介绍自己为什么选择创业,文章最早发表在我们的微信公众号上,支持原创,详见这里, 欢迎关注微信公众号blackerteam,更多详见www.rtc.help 2016.06.01对公司来说是个很 ...
- xpath表达式,提取标签下的全部内容(将其他标签过滤)
例如要提取span下的内容 //div[@class="content"]/span 正确的其中一种写法如下data = response.xpath('//div[@class= ...
- 第五百八十六天至第六百零五天 how ccan I 坚持
考研中,勿扰... 我是个逗比,哈哈. 时间不够用了呢,哎.
- git服务器的搭建
http://blog.jobbole.com/25944/ 1,概念 git服务器:就是一个仓储,一个大家都可以访问的公共仓储,大家可以从这个仓储中拉取和推送数据. 协议: 与gist服务通讯的仓储 ...
- nginx 设置反响代理实现nginx集群
ginx.conf: worker_processes 1; events { worker_connections 1024; } http { include mime.types ...
- js 验证码 倒计时60秒
js 验证码 倒计时60秒 <input type="button" id="btn" value="免费获取验证码" /> & ...
- Python笔记——break的注意事项
在python中有个控制流的语句:break 它是用来终止循环语句的,不管此时循环体进行到哪,只要碰到break都停止执行循环语句. 1.举例脚本: #!/usr/bin/env python for ...
- 算法与数据结构实验题 5.2 Missile
1.题目: 2.解题思路: 把每个点对应的两条半径求出,之后对d1进行升序排序,对应d2也改变位置.其中一个圆心的半径r1确定之后,除去第一个圆包围的点,在其余点中找到另外一个圆的最长的半径r2,此时 ...
- php中Closure::bind用法(手册记录)
手册中 Closure::bind — 复制一个闭包,绑定指定的$this对象和类作用域. 具体参数可以看手册,这里记录下这个方法的实际用处. <?php trait MetaTrait { p ...