《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 ...
随机推荐
- Android IOS WebRTC 音视频开发总结(七四)-- WebRTC开源5周年了,Google怎么看?
本文最早发表在我们的微信公众号上(微信ID:blackerteam),支持原创,详见这里, 2016年6月9日是WebRTC开源5周年的日子,Google WebRTC负责人Harald在社区里面写了 ...
- js加载顺序
在jsp页面中引js文件,页面按照js顺序加载,若js中存在相同的方法,应该会执行顺序在前的js中的方法.有时候遇到js方法不执行或者执行不对的情况,可能就是js的顺序问题,有时候会找很久找不到问题. ...
- Hadoop HDFS编程 API入门系列之HdfsUtil版本1(六)
不多说,直接上代码. 代码 package zhouls.bigdata.myWholeHadoop.HDFS.hdfs2; import java.io.FileOutputStream;impor ...
- java中Thread的 interrupt异常处理
http://blog.csdn.net/srzhz/article/details/6804756
- MS sql server 基础知识回顾(二)-表连接和子查询
五.表连接 当数据表中存在许多重复的冗余信息时,就要考虑将这些信息建在另一张新表中,在新表中为原表设置好外键,在进行数据查询的时候,就要使用到连接了,表连接就好像两根线,线的两端分别连接两张表的不同字 ...
- oracle 客户端重新安装遇到的问题
前一阵,因为把ORACLE客户端的密码忘记了,在网上也找了好多方法,试着不重新安装找回密码,可是都行不通,没有办法重新装.安装时遇到一些问题,因为我是WIN7,64位系统,安装的oracle11g64 ...
- 使用阿里Docker镜像加速器加速
在阿里开发者平台注册开发者账号 https://dev.aliyun.com/search.html 注册之后可以访问Docker镜像服务 https://cr.console.aliyun.com/ ...
- JS出现illegal character非法字符提示
引用js文件,js文件内的汉字在页面显示乱码 解决方式: a. 保持js文件编码与jsp页面编码格式一致: b. 在引入js文件时,在script中添加charset=""属性,指 ...
- 计算机病毒实践汇总二:bufferzone沙盘使用体验
在尝试学习分析的过程中,判断结论不一定准确,只是一些我自己的思考和探索.敬请批评指正! 1. 安装bufferzone及其简单使用 (1)安装BufferZone BufferZone的msi安装文件 ...
- python 注册
1.打开网址,点击 获得注册码 http://idea.qinxi1992.cn/ 2.help -- register 第二步: http://jetbrains.tencent.click/ ...