原文地址:Linux应用程序开发 基础知识 作者:chipmunk_byr
Table of Contents
1. C语言基础 1.1. 数据类型 1.2. 关键字 1.3. 变量等级 1.4. 特殊字符的表示方法: 1.5. 格式化字符串 1.6. 指针与数组 1.7. 结构体 1.8. typedef--自定义类型名 1.9. 函数和宏 1.10. ANSI标准头文件 2. 预处理 3. 使用GCC编译程序 4. 使用gdb调试程序 5. Linux程序开发基础 5.1. 路径 5.2. 库文件 5.3. 预处理 5.4. 系统调用(system call) 6. 文件处理 7. Linux环境编程 7.1. 参数选项 7.2. 环境变量 7.3. 时间 7.4. 临时文件 7.5. 用户信息 7.6. 日志信息 8. 进程 8.1. 进程状态 9. 串口编程 9.1. 常用函数 9.2. 设置串口属性 9.3. c_iflag输入标志说明 9.4. c_oflag输出标志说明 9.5. c_cflag控制模式标志说明 9.6. c_cc[]控制字符说明 9.7. c_lflag本地模式标志说明 9.8. 下面介绍一些常用串口属性的设置方法。 10. 安全 10.1. 内核漏洞介绍 11. 数据结构(Data Structure) 11.1. 基础概念 11.2. 线性数据结构 12. 网络编程 12.1. TCP/IP协议分析 12.2. 入门示例程序 13. 存储器寻址 14. 设备驱动程序开发 15. 字符设备驱动程序 15.1. 设备号 15.2. 设备号的分配和释放 15.3. 重要的数据结构 15.4. 读和写 16. PCI设备 17. 内核初始化优化宏 18. 访问内核参数的接口 19. 内核初始化选项 20. 内核模块编程 20.1. 入门 20.2. 为模块添加描述信息 20.3. 内核模块处理命令介绍 21. 网络子系统 21.1. sk_buff结构 21.2. sk_buff结构操作函数 21.3. net_device结构 21.4. 网络设备初始化 21.5. 网络设备与内核的沟通方式 21.6. 网络设备操作层的初始化 21.7. 内核模块加载器 21.8. 虚拟设备 21.9. 8139too.c源码分析 21.10. 内核网络数据流 22. 备忘录List of Tables
1.1. 特殊字符的表示方法Table of Contents
1.1. 数据类型 1.2. 关键字 1.3. 变量等级 1.4. 特殊字符的表示方法: 1.5. 格式化字符串 1.6. 指针与数组 1.7. 结构体 1.8. typedef--自定义类型名 1.9. 函数和宏 1.10. ANSI标准头文件Linux是使用C语言开发的,基于Linux平台的应用程序开发,C语言是首选的开发语言。本章记录C语言的基本概念和基础知识。
整数类型(int),
各种整数数制表示法:
-
ddd,十进制表示法,d为0--9的整数,但不能以0开头。如:123,345。
-
0ooo,八进制表示法,以0(数字0)开头,o为0--7的整数。如:010(八进制)=8(十进制),014(八进制)=12(十进制)。
-
0xhhh,十六进制表示法,以0x或0X开头,h为0--9、A、B、C、D、E、F。如:0x10(十六进制)=16(十进制),0xA(十六进制)=10(十进制)。
-
以L或l结尾的数表示长整数(long int),编译器会以32位空间存放此数字,但GCC默认是以32位存放整数,所以此表示法在Linux下没什么作用。
关键字是C语言本身保留使用的,不能用于变量和函数名。
auto double int structbreak else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while
-
auto,内部变量,在函数内部声明。只能在函数内部使用,它的生命周期从调用函数开始,到函数执行完时消失。内部变量以堆栈存放,必须在函数执行时才会存在,这种方式称为声明。auto可省略。如:
auto int i = 0;
/* 可写成int i = 0; */
内部变量的优缺点:
-
内部变量只在函数内有效,能提高函数的安全。
-
内部变量在函数结束时消失,不会长期占用内存空间,能提高内存的利用率。
-
内部变量的缺点是生命周期短,函数运行结束后不能保留。
-
-
staticauto,内部静态变量,在函数内部定义,auto也可省略。内部静态变量以固定地址存放,编译时就已分配置内在空间,这种方式称为定义。由于有固定地址,函静态变量不会随函数的结束而消失。static变量会一直保存在内存空间中,当函数再次执行时,上次保留的使用静态变量可以继续使用。如:
static int i = 0;
-
extern,外部变量,是在函数外定义的变量,可被多个函数存取。在外部变量定义覆盖范围之内的函数内可以自由使用外部变量。不在外部变量定义覆盖范围之内的函数要使用外部变量就要先使用extern关健字来声明外部变量。
int i; /* 外部变量定义,在main函数外 */
int main(void)
{
i = 1; /* main()函数位于外部变量i定义的下面,不用声明可直接使用 */
printf("%d\n", i);
}
不在外部变量定义覆盖范围之内的函数要使用外部变量就要先使用extern关健字来声明外部变量。
int main(void)
{
extern int i; /* 外部变量i在main()函数之后定义,需用extern关键字声明后才能使用 */
i = 1;
printf("%d\n",i);
}
int i;
...
在另外的程序文件中我们也可以通过扩展声明使用其它程序文件中的外部变量。
程序1 hello.c
#include <stdio.h>
int main(void)
{
extern int i; //扩展声明外部变量
i = 333;
printf("%d\n", i);
extern des(void); //扩展声明外部函数
des();
}
int i; //外部变量定义
程序2 hello1.c
#include <stdio.h>
extern int i; //扩展声明其它程序文件中的外部变量
void des()
{
i++;
printf("%d\n",i);
}
编译
debian:~/c# gcc hello.c hello1.c
debian:~/c# ./a.out
333
334
外部变量有效范围总结:
-
由外部变量定义的位置开始,至文件结尾。
-
不在有效范围内的函数,也可通过extern扩展声明使用定义的外部变量,且可在多个函数中使用。注意:在各函数中使用的外部变量是一样的,对该变量的修改会影响到其它函数内的同一变量。
-
可用extern扩展声明使用另外一个程序文件中的外部变量。
外部变量的优点是生命周期长,可在函数间共享数据和传输数据。缺点是变量安全性较低,但可通过合理设置外部变量的有效范围提高安全性。
-
-
static extern,外部静态变量,在函数外部定义,只供单一程序文件使用,即使其它程序文件定义了同样名称的变量,编译器也把它当成另外一个变量处理。外部静态变量能有效隔离变量在一个程序文件中。
static int i;
-
register,register变量是以寄存器(register)来存放变量,而不是一般内存。只有内部变量才能使用register类型变量。使用这种变量能加快变量的处理速度。但缺点是要占用CPU寄存器。如:
register int i;
register int j;
变量等级的概念也同样适用于函数。若想调用不在有效范围内的函数,则要用extern扩展声明函数的有效范围。
内部变量是以堆栈方式存放的,必须在函数执行时才会存在,所以称为声明(Declaration)。其它如staticauto、extern和staticextern等级的变量,都是以固定的地址来存放的,而不是以堆栈方式存放的,在程序编译时就已分配了空间,所以称之为定义(Definition)。
-
C语言中专门用来存放内存地址的变量叫指针(pointer)变量,简称指针。
-
&运算符用来取得变量地址,
-
"*"运算符用来取得指针变量的值。
-
数组名就是地址变量,指向内存中存放第一个数组元素的地址。数组元素编号从0开始,如a[0]表示数组a的第一个元素。
数组是内存中的连续区间,可根据声明类型存放多种数值类型。如:
int a[10]; 声明一个有10个int元素的数组char b[20]; 声明一个有20个char元素的数组
指针示例:
int *p; /* p是一个指针,p的内容是内存的地址,在这个地址中将存放一个整数。数组名和指针都是用来存放内存地址的,不过数组名具有固定长度,不可变。而指针与一般变量一样,其值是可变的。
结构体是用户定义的由基本数据类型组成的复合式数据类型。数组也是复合式数据类型,但二者是不同的,数组是相同类型数据的集合,而结构体是不同类型数据的集合。如我们可以把一个人的姓名、性别,年龄组成一个单一结构体。这样在程序处理时就把它当成一个独立对象进行处理。
结构体声明方法有两种,一种是分离式声明,一种是结合式声明。分离式声明是先把声明结构体,在程序中再声明结构体变量。结合式声明是把结构体声明和变量声明同时完成。
分离式声明示例struct person{
char name;
char ***;
int age;
};
main(void){
struct person worker;
...
}
结合式声明示例
struct person{
char name;
char ***;
int age;
}worker;
每个结构体可以表示一个工人的信息,如果要表示多个工人的信息,则可以用结构体数组。
struct person{char name;
char ***;
int age;
};
main(void){
struct person worker[20]; //表示20个工人
...
}
结构体初始设置。
struct person{char name;
char ***;
int age;
}worker={"jims","male",30};
用"."和"->"运算符存取结构体中的数据。"."是直接存取法,"->"为间接存取法,用于结构体指针。如果p是一个指向person结构体的指针,则p->name和(*p).name的结果是一样的。
结构体可以自定义数据类型,而typedef可以自定义新的类型名。如:
#include <stdio.h>typedef char *STRING; //定义一个新的字符指针类型名STRING
main(void){
STRING a;
a = "abc";
printf("the a value is %s.\n",a);
}
a为字符指针类型,自定义类型名通常以大写方式表示,以示区别。
#define与typedef的区别是:#define只是单纯地进行变量替换,而typedef是创建新的类型名。typedef的一个主要作用是简化声明,提高程序的可读性。如:
typedef struct person{char name;
char ***;
int age;
} p
这样我们就定义一个新的结构体类型名p,在程序中我们可以使用它来声明变量。如
main(void){p worker;
worker = {"jims","male",30};
}
函数是C代码的集合,每个C程序由一个或多个函数组成,main()是一个特殊的函数,是C程序的入口,每个C程序必须有且只能有一个mian()函数。
ANSI函数定义:
类型 函数名(类型 参数1,类型 参数2, ...){
函数代码;
}
示例:
int func(int i, char c)
{
...
}
在程序中要使用我们设计开发的函数,需要先进行声明,函数声明的作用是把函数类型告诉编译器。函数声明与定义差不多,只是不包括程序主体。上面示例的函数在主程序中的声明方式如下:
void main(){
int total;
int func(int i, char c); //函数声明
total = int(xxx,xxx); //声明后才能调用该函数
}
定义和声明中的参数类型(int,char)要相同,但名称(i,c)可以不同。 |
当函数没有返回值时,需声定义成void类型,调用者也要做void声明。
一般我们把函数的声明放在一个统一的文件中,这个文件叫头文件。在程序中用#include命令把头文件包含进来。在程序中调用函数前就不用再进行函数声明了。头文件简化了函数声明的管理并使头文件可被多个程序重复使用。大大提高C程序的开发效率。例如:我们最常使用的printf()函数,在使用前我们不需每次都进行声明操作,直接使用就可以啦。但前提是我们要把stdio.h头文件包含进来。printf()函数声明在stdio.h文件中已进行了声明。
C语言在程序进行编译之前,会先将程序中以"#"标记的部份进行处理。这种处理叫做预处理。预处理主要的完成以下三个内容:宏处理、头文件和条件式编译。
-
宏处理指令语法如下:
#define 宏名 字符串
示例:
#define MAX 200
宏指令语句尾不用加分号(;) 宏定义可以用#undef命令取消,我们可以用该功能进行程序调试。
-
头文件处理是把头文件中的函数声明插入程序中。
-
条件式编译,编译器可根据条件式编译语句有选择地进代码块进行编译。选择式编译指令如下:
#if 表达式 如果表示式结果不为0,则编译下面的程序
#ifdef 宏名 若宏名已被定义,则编译下面的程序
#ifndef 宏名 若宏名未定义,则编译下面的程序
#else 前面条件不成立时,则编译下面的程序
#endif 结束上列各种条件式编译
直接生成a.out可执行文件
debian:~/c# gcc hello.c编译hello.c程序,生成hello可执行文件:
debian:~/c# gcc -o hello hello.c生成.s的汇编代码文件。
debian:~/c# gcc -S hello.c如果想利用gdb工具来调试程序,在编译程序时要使用-g选项。如:
debian:~/c# gcc -g serial.c -o serial调试serial程序。
debian:~/c# gdb serialGNU gdb 6.5-debian
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) list
8 #include <errno.h> /*错误号定义*/
9
10 int main(void)
11 {
12 int fd,n,status,buffsize;
13 struct termios a;
14 struct termios *oldtio;
15 char m[255],*comm;
16
17 fd = open("/dev/ttyS0",O_RDWR|O_NOCTTY|O_NDELAY);
(gdb)
gdb的list命令是列出程序源码。下面介绍gdb下的各种操作。
-
list,列出程序源代码,一次只列出10行的内容。list命令可以指定范围。如:list 5,10可列出第5行到第10行的内容。
-
run,执行程序。按Ctrl+c可中断程序的执行。
-
shell,暂时退出gdb回到shell环境。在shell环境用exit命令可以返回gdb。
-
break,设置断点,后跟行号则把断点设置在指定的行号,后跟函数名则把断点设置在函数。如break 6,breakfunction。还可根据条件设置断点,如:break 9 if result >50。这条命令的意思是,当运行到第9行时,如果result变量的值大于50,则中断程序。
(gdb) break 6
Breakpoint 1 at 0x8048634: file serial.c, line 6.
-
watch,指定条件,如果成立则中断。如:watch result >50。当result的变量大于50时,马上中断程序。
-
print,打印变量值,如:print result。
-
whatis,查看变量类型,如:whatis result。
-
continue,从中断点继续运行程序。
-
step,从中断点开始单步运行,如果遇到函数,则进入函数单步运行。
-
next,从中断点开始单步运行,如果遇到函数,则运行函数,该命令不会进入函数单步运行,而是运行整个函数。
-
info breakpoints,查看程序中所设置的所有中断点信息。
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048634 in main at serial.c:6
Enb字段是"y",表示断点1现正生效。
-
disable/enable,控制中断点失效和启用。如:disable 1。如果disable/enable命令后没有指定断点号,则该命令作用于所有已设置的断点。
(gdb) disable 1
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep n 0x08048634 in main at serial.c:6
Enb字段由"y"变成"n",断点1暂时被禁止。
-
enable once,使断点生效一次。
-
delete,删除断点。如:delete 1。delete要指定断点号。
-
clear,删除断点。如:clear 6。clear要指定设置断点的行号或函数名。
-
help all,显示所有gdb环境的命令。
在gdb环境下,按tab键可自动补全命令。直接按回车键可重复执行上一个操作。按上下光标键可显示历史命令。 |
在设置Linux的系统路径时,使用冒号分隔每个路径名。如:
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11"在Linux中的程序有两种,一种是可执行程序,与Windows下的.exe文件类似,一种是脚本,与Windows下的.bat文件类似。
Linux中常用的程序存放路径有以下几个:
-
/bin,该路径存放系统启动时需要使用的程序。
-
/usr/bin,该路径存放用户需使用的标准程序。
-
/usr/local/bin,该路径存放本地安装的程序。
-
Linux使用斜杠"/"分隔路径名,而不是Windows的反斜杠"\"。
-
Linux下的C编译器使用GCC,由于历史的原因,在POSIX兼容的操作系统中,C编译器都叫cc,所以Linux下也有一个cc命令,它是一个到gcc的软链接。
开发工具,多数位于/usr/bin或/usr/local/bin目录下。
头文件,位于/usr/include目录。头文件包含有常量定义、系统调用和库函数调用的声明。这是系统默认的头文件存放路径,在编译程序时,编译器会自动查找该目录。gcc编译器在编译程序时也可用-I参数指定另外的头文件路径。如:
gcc -I/usr/local/myinclude test.c。库文件,库是一组已编译的函数集合,可方便我们重用代码。默认存放在/lib和/usr/lib目录。库文件可分为静态和共享两类。
-
.a,静态库文件。使用静态库将会把所有的库代码引入程序,占用更多的磁盘空间和内存空间,所以一般建议使用共享库。
-
.so,共享库文件。使用共享库的程序不包含库代码,只在程序运行才调用共享库中的代码。
在编译时可用包含路径的库文件名或用-l参数指定使用的库文件,/usr/lib/libm.a等价于-lm。如:
gcc -o hello hello.c /usr/lib/libm.a或用-l参数写成
gcc -o hello hello.c -lm
如果我们要使用的库文件不在默认位置,在编译程序时可用-L参数指定库文件的路径。下面例子使用了/usr/hello/lib目录下的libhello库文件:
gcc -o hello -L/usr/hello/lib hello.c -lhello创建和使用静态库。
-
分别创建两个函数,函数a的内容如下:
#include <stdio.h>
void a(char *arg)
{
printf("function a,hello world %s\n",arg);
}
函数b的内容如下:
#include <stdio.h>
void b(int arg)
{
printf("function b,hello world %d\n",arg);
}
-
接着,生成两个对象文件。
debian:~/c# gcc -c a.c b.c
debian:~/c# ls *.o
a.o b.o
-
最后,用ar归档命令把生成的对象文件打包成一个静态库libhello.a。
debian:~/c# ar crv libhello.a a.o b.o
r - a.o
r - b.o
-
为我们的静态库定义一个头文件lib.h,包含这两个函数的定义。
/*
* this is a header file.
*/
void a(char *arg);
void b(int arg);
}}}
* 创建jims.c程序,内容如下。{{{#!cplusplus
#include "lib.h"
int main()
{
a("jims.yang");
b(3);
exit(0);
}
-
利用静态链接库编译程序。
debian:~/c# gcc -c jims.c
debian:~/c# gcc -o jims jims.o libhello.a
debian:~/c# ./jims
function a,hello world jims.yang
function b,hello world 3
debian:~/c#
gcc -o jims jims.o libhello.a也可以写成gcc -o jims jims.o -L. -lhello。
共享库比静态库具有以下的优点:
-
当多个进程使用同一共享库时,Linux会把共享库中存放可执行代码的内存进行共享。所以共享库可节省内存,提高系统性能。
-
程序可共享代码,减少磁盘空间占用。
-
共享库出错,只要重新编译共享库即可,不用重新编译应用程序。
ldconfig程序用来安装一个共享库,。
只有在为系统库安装一个库的时候,才需要在/etc/ld.so.conf中创建记录,并运行ldconfig更新共享库的缓存。
LD_LIBRARY_PATH环境变量用来指定附加的库文件路径。系统默认的库文件路径位于/usr/lib和/lib目录下。
LD_PRELOAD环境变量指定提前载入的库,用于替代系统库。
预处理,在程序开头以“#”开头的命令就是预处理命令,它在语法扫描和分析法时被预处理程序处理。预处理有以下几类:
-
宏定义,用#define指令定义。如:#define BUFFER 1024。取消宏定义用#undef指令。宏还可带参数,如:
#define BUF(x) x*3
-
包含头文件,用#include指令,可把包含的文件代码插入当前位置。如:
<#include <stdio.h>。
包含的文件可以用尖括号,也可用双引号,如:
#include "stdio.h"。
不同之处是,使用尖括号表示在系统的包含目录(/usr/include)下查找该文件,而双引号表示在当前目录下查找包含文件。每行只能包含一个包含文件,要包含多个文件要用多个#include指令。
-
条件编译,格式如下:
格式一,如果定义了标识符,则编译程序段1,否则编译程序段2:
#ifdef 标识符
程序段1
#else
程序段2
#endif
格式二,如果定义了标识符,则编译程序段2,否则编译程序段1,与格式一相反:
#ifndef 标识符
程序段1
#else
程序段2
#endif
格式三,常量表达式为真则编译程序段1,否则编译程序段2:
#if 常量表达式
程序段1
#else
程序段2
#endif
使用gcc编译程序时,要经过四个步骤。
-
预处理(Pre-Processing),用-E参数可以生成预处理后的文件。
debian:~/c# gcc -E hello.c -o hello.i
-
编译(Compiling)
-
汇编(Assembling)
-
链接(Linking)
GCC默认将.i文件看成是预处理后的C语言源代码,所以我们可以这样把.i文件编译成目标文件。
debian:~# gcc -c hello.i -o hello.o}}}在GCC中使用-pedantic选项能够帮助程序员发现一些不符合ANSI/ISO C标准的代码,但不是全部。从程序员的角度看,函数库实际上就是一些头文件(.h)和库文件(.so或者.a)的集合。
要理解系统调用就要先理解程序代码运行的两种模式,一种是用户模式,一种是内核模式。我们编写的应用程序运行在用户模式下,而设备驱动程序和文件系统运行在内核模式。在用户模式下运行的程序受到严格的管理,不会破坏系统级应用。而在内核模式下运行的程序可以对电脑有完全的访问权。系统调用就是运行在内核模式下的代码为运行在用户模式下的代码提供服务。
系统调用的错误返回码是负数,定义在>errno.h<文件中。在系统调用中发生错误,C函数库就会用错误码填充全局变量errno。用perror()和strerror()函数可以输出错误信息。
系统调用多数在>unistd.h<中定义。
在Linux系统内所有东西都是以文件的形式来表示的,除一般的磁盘文件外,还有设备文件,如硬盘、声卡、串口、打印机等。设备文件又可分为字符设备文件(character devices)和块设备文件(block devices)。使用man hier命令可以查看Linux文件系统的分层结构。文件的处理方法一般有五种,分别是:
-
open,打开一个文件或设备。
-
close,关闭一个打开的文件或设备。
-
read,从一个打开的文件或者设备中读取信息。
-
write,写入一个文件或设备。
-
ioctl,把控制信息传递给设备驱动程序。
open,close,read,write和ioctl都是低级的,没有缓冲的文件操作函数,在实际程序开发中较少使用,一般我们使用标准I/O函数库来处理文件操作。如:fopen,fclose,fread,fwrite,fflush等。在使用标准I/O库时,需用到stdio.h头文件。
-
fopen()这个标准I/O库函数用于打开文件,在Linux中文件要先打开后才能进行读写操作。
#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
*mode选项:
"r" 或o"rb" 为读打开文件
"w" 或 "wb" b为写打开文件,如果文件不存在则创建,如果存在则覆盖
"a" 或 "ab" b为追加内容而打开文件
"r+" 或o"rb+" 或 "r+b"r 为更新打开文件,不会覆盖旧文件
"w+" 或 "wb+"b或 "w+b"w 为更新打开文件,会覆盖旧文件
"a+"a或 "ab+" 或 "a+b" 为更新打开文件,更新内容追加到文件末尾
一些常用的文件和目录维护函数:chmod、chown、unlink、link、symlink、mkdir、rmdir、chdir、getcwd、opendir,closedir、readdir、telldir、seekdir等。
fcntl用于维护文件描述符,mmap用于分享内存。
创建文档并输入信息的示例代码:
#include <stdio.h>main(void)
{
FILE *fp1;
char c;
fp1 = fopen("text.txt","w");
while ((c = getchar())!= '\n')
putc(c,fp1);
fclose(fp1);
}
显示路径的示例代码
#include <unistd.h>#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char *topdir = ".";
if (argc >= 2)
topdir = argv[1];
printf("Directory scan of %s\n", topdir);
printdir(topdir,0);
printf("done.\n");
exit(0);
}
printdir(char *dir, int depth)
{
DIR *dp;
struct dirent *entry;
struct stat statbuf;
if((dp = opendir(dir)) == NULL)
{
fprintf(stderr,"cannot open directory:%s\n",dir);
return;
}
chdir(dir);
while((entry = readdir(dp)) != NULL)
{
lstat(entry->d_name,&statbuf);
if(S_ISDIR(statbuf.st_mode))
{
if(strcmp(".",entry->d_name) == 0 || strcmp("..",entry->d_name) == 0)
continue;
printf("%*s%s/\n",depth,"",entry->d_name);
printdir(entry->d_name,depth+4);
}
else printf("%*s%s\n",depth,"",entry->d_name);
}
chdir("..");
closedir(dp);
}
void main()表示程序没有参数,int main(int argc, char *argv[])表示程序要带参数,argc保存着参数的个数,argv[]数组保存着参数列表。如:
debian:~# mytest a b cargc: 4
argv: ["mytest","a","b","c"]
getopt()函数和getopt_long()用来处理程序选项。getopt_long()函数可以处理以"--"开头的选项。Gnu官方手册页:http://www.gnu.org/software/libc/manual/html_node/Getopt.html
获取命令行参数的示例代码:
#include <stdio.h>#include <unistd.h>
int main(int argc, char *argv[])
{
int opt;
while((opt = getopt(argc,argv,"if:lr")) != -1) /* 返回“-1”表示已没选项需要处理。*/
{
switch(opt){
case 'i':
case 'l':
case 'r':
printf("option: %c\n", opt);
break;
case 'f':
printf("filename: %s\n", optarg); /*如果选项需要一个参数,则参数存放在外部变量optarg中。*/
break;
case ':':
printf("option needs a value \n"); /*“:”表示选项需要参数*/
break;
case '?':
printf("unknown option: %c\n", optopt); /*返回“?”表示无效的选项,并把无效的选项存放在外部变量optopt中。*/
break;
}
}
for(; optind < argc; optind++) /*外部变量optind指向下一个要处理的选项索引值。*/
printf("argument: %s\n", argv[optind]);
}
在bash shell中使用set命令可以列出Linux系统的环境变量,在C程序中我们也可以用putenv()和getenv()函数来获取Linux系统的环境变量。这两个函数的声明如下:
char *getenv(const char *name);int putenv(const char *string);
系统有一个environ变量记录了所有的系统变量。下面的示例代码可把environ的值显示同来。
#include <stdlib.h>#include <stdio.h>
extern char **environ;
int main()
{
char **env = environ;
while(*env)
{
printf("%s\n",*env);
env++;
}
}
linux和其它unix一样,使用GMT1970年1月1日子夜作为系统时间的开始,也叫UNIX纪元的开始。现在的时间表示为UNIX纪元至今经过的秒数。
#include <time.h>time_t time(time_t *t);
显示系统时间的示例代码:
#include <time.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
int i;
time_t the_time;
for(i = 1; i <= 10; i++){
the_time = time((time_t *)0);
printf("%d the time is %ld\n", i, the_time);
sleep(2);
}
}
用ctime()函数以友好方式返回当前时间,它的函数声明格式:
#include <time.h>char *ctime(const time_t *timeval);
示例:
#include <time.h>
#include <stdio.h>
int main(void)
{
time_t time1;
(void)time(&time1);
printf("The date is: %s\n",ctime(&time1));
}
程序输出:
The date is: Thu Dec 7 09:58:23 2006
用localtime()函数可以返回本地时间,它是一个tm结构,tm结构体的内容如下:
struct tm{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
};
int tm_sec Seconds, 0-61
int tm_min Minutes, 0-59
int tm_hour Hours, 0-23
int tm_mday Day in the month, 1-31
int tm_mon Month in the year, 0-11(January= 0)
int tm_year Years since 1900
int tm_wday Day in the week, 0-6. (Sunday = 0)
int tm_yday Day in the year, 0-365
int tm_isdst Daylight savings in effect
localtime()函数的使用方法如下:
函数声明:#include <time.h>
struct tm *localtime(const time_t *timeval);
示例代码:
#include <time.h>
#include <stdio.h>
int main(void)
{
time_t time1;
struct tm *p;
time1 = time(NULL);
printf("The ctime is: %s\n",ctime(&time1));
p = localtime(&time1);
printf("The localtime is:\n tm_year+1900 = %d年\n tm_mon = %d月\n tm_mday = %d日\n wday = %d\n hour = %d 时\n min = %d分\n sec = %d秒\n",p->tm_year+1900,p->tm_mon,p->tm_mday,p->tm_wday,p->tm_hour,p->tm_min,p->tm_sec);
}
运行结果:
The ctime is: Thu Dec 7 10:31:36 2006
The localtime is:
tm_year+1900 = 2006年
tm_mon = 11月
tm_mday = 7日
wday = 4
hour = 10 时
min = 31分
sec = 36秒
用mkstemp()函数创建临时文件。
#include<stdlib.h>int mkstemp(char * template);
示例:
#include <stdio.h>
int main(void)
{
char template[] = "template-XXXXXX";
int fp;
fp = mkstemp(template);
printf("template = %s\n", template);
close(fp);
}
获取用户信息。
声明:#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid); /* 根据uid返回用户信息 */
struct passwd *getpwnam(const char *name); /* 根据用户名返回用户信息 */
passwd结构体说明:
passwd Member Description
char *pw_name The user's login name
uid_t pw_uid The UID number
gid_t pw_gid The GID number
char *pw_dir The user's home directory
char *pw_gecos The user's full name
char *pw_shell The user's default shell
示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <stdio.h>
#include <pwd.h>
int main(void)
{
uid_t uid;
gid_t gid;
struct passwd *pw;
uid = getuid();
gid = getgid();
pw = getpwuid(uid);
printf("User is %s\n", getlogin());
printf("The uid is:%d\n", uid);
printf("The gid is:%d\n",gid);
printf("The pw struct:\n name=%s, uid=%d, gid=%d, home=%s,shell=%s\n", pw->pw_name, pw->pw_uid, pw->pw_gid, pw->pw_dir, pw->pw_shell);
}
用gethostname()函数获取主机名。
函数声明:#include <unistd.h>
int gethostname(char *name, size_t namelen); /* 主机名返回给name变量 */
示例代码:
#include <stdio.h>
#include <unistd.h>
int main(void)
{
char computer[100];
int status;
status = gethostname(computer, 100);
printf("The status is %d\n", status);
printf("The hostname is: %s\n", computer);
}
用uname()函数获取主机详细信息,就像shell的uname命令返回的信息一样。
函数声明:#include <sys/utsname.h>
int uname(struct utsname *name);
utsname结构体说明:
utsname Member Description
char sysname[] The operating system name
char nodename[] The host name
char release[] The release level of the system
char version[] The version number of the system
char machine[] The hardware type
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>
int main(void)
{
char computer[100];
int status;
struct utsname uts;
status = gethostname(computer,100);
printf("The computer's size is %d\n",sizeof(computer));
printf("The status is %d\n", status);
printf("The hostname is: %s\n", computer);
uname(&uts);
printf("The uname's information.\n uts.sysname=%s\n uts.machine=%s\n uts.nodename=%s\n uts.release=%s\n uts.version=%s\n", uts.sysname,uts.machine,uts.nodename,uts.release,uts.version);
}
使用syslog()函数处理日志信息。
函数声明:#include <syslog.h>
void syslog(int priority, const char *message, arguments...);
priority参数的格式(severity level|facility code)
示例:
LOG_ERR|LOG_USER
severity level:
Priority Level Description
LOG_EMERG An emergency situation
LOG_ALERT High-priority problem, such as database corruption
LOG_CRIT Critical error, such as hardware failure
LOG_ERR Errors
LOG_WARNING Warning
LOG_NOTICE Special conditions requiring attention
LOG_INFO Informational messages
LOG_DEBUG Debug messages
facility value(转自syslog.h头文件):
/* facility codes */
#define LOG_KERN (0<<3) /* kernel messages */
#define LOG_USER (1<<3) /* random user-level messages */
#define LOG_MAIL (2<<3) /* mail system */
#define LOG_DAEMON (3<<3) /* system daemons */
#define LOG_AUTH (4<<3) /* security/authorization messages */
#define LOG_SYSLOG (5<<3) /* messages generated internally by syslogd */
#define LOG_LPR (6<<3) /* line printer subsystem */
#define LOG_NEWS (7<<3) /* network news subsystem */
#define LOG_UUCP (8<<3) /* UUCP subsystem */
#define LOG_CRON (9<<3) /* clock daemon */
#define LOG_AUTHPRIV (10<<3) /* security/authorization messages (private) */
#define LOG_FTP (11<<3) /* ftp daemon */
示例代码:
#include <syslog.h>
#include <stdio.h>
int main(void)
{
FILE *f;
f = fopen("abc","r");
if(!f)
syslog(LOG_ERR|LOG_USER,"test - %m\n");
}
上面的日志信息由系统自动给出,我们也可过滤日志信息。用到以下函数:
#include <syslog.h>void closelog(void);
void openlog(const char *ident, int logopt, int facility);
int setlogmask(int maskpri);
logopt参数的选项:
logopt Parameter Description
LOG_PID Includes the process identifier, a unique number allocated to each process by the system, in the messages.
LOG_CONS Sends messages to the console if they can’t be logged.
LOG_ODELAY Opens the log facility at first call to .
LOG_NDELAY Opens the log facility immediately, rather than at first log.
示例代码:
#include <syslog.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int logmask;
openlog("logmask", LOG_PID|LOG_CONS, LOG_USER); /*日志信息会包含进程id。*/
syslog(LOG_INFO, "informative message, pid=%d", getpid());
syslog(LOG_DEBUG,"debug message, should appear"); /*记录该日志信息。*/
logmask = setlogmask(LOG_UPTO(LOG_NOTICE)); /*设置屏蔽低于NOTICE级别的日志信息。*/
syslog(LOG_DEBUG, "debug message, should not appear"); /*该日志信息被屏蔽,不记录。*/
}
不同安全级别的日志信息存放在/var/log目录下的哪个文件中是由/etc/syslog.conf文件控制的,下面是我系统中syslog.conf文件的内容:
# /etc/syslog.conf Configuration file for syslogd.#
# For more information see syslog.conf(5)
# manpage.
#
# First some standard logfiles. Log by facility.
#
auth,authpriv.* /var/log/auth.log
*.*;auth,authpriv.none -/var/log/syslog
#cron.* /var/log/cron.log
daemon.* -/var/log/daemon.log
kern.* -/var/log/kern.log
lpr.* -/var/log/lpr.log
mail.* -/var/log/mail.log
user.* -/var/log/user.log
uucp.* /var/log/uucp.log
#
# Logging for the mail system. Split it up so that
# it is easy to write scripts to parse these files.
#
mail.info -/var/log/mail.info
mail.warn -/var/log/mail.warn
mail.err /var/log/mail.err
# Logging for INN news system
#
news.crit /var/log/news/news.crit
news.err /var/log/news/news.err
news.notice -/var/log/news/news.notice
#
# Some `catch-all' logfiles.
#
*.=debug;\
auth,authpriv.none;\
news.none;mail.none -/var/log/debug
*.=info;*.=notice;*.=warn;\
auth,authpriv.none;\
cron,daemon.none;\
mail,news.none -/var/log/messages
#
# Emergencies are sent to everybody logged in.
#
*.emerg *
#
# I like to have messages displayed on the console, but only on a virtual
# console I usually leave idle.
#
#daemon,mail.*;\
# news.=crit;news.=err;news.=notice;\
# *.=debug;*.=info;\
# *.=notice;*.=warn /dev/tty8
# The named pipe /dev/xconsole is for the `xconsole' utility. To use it,
# you must invoke `xconsole' with the `-file' option:
#
# $ xconsole -file /dev/xconsole [...]
#
# NOTE: adjust the list below, or you'll go crazy if you have a reasonably
# busy site..
#
daemon.*;mail.*;\
news.crit;news.err;news.notice;\
*.=debug;*.=info;\
*.=notice;*.=warn |/dev/xconsole
Table of Contents
8.1. 进程状态进程是任何正在运行的程序代码,它是操作系统的基本调度单位,只有它能在CPU上运行。对于一个进程,内核记录以下信息:
-
进程运行的当前位置。
-
进程正在访问的文件。
-
进程的所属的用户和组。
-
进程的当前目录。
-
进程访问的内存空间状况。
Table of Contents
9.1. 常用函数 9.2. 设置串口属性 9.3. c_iflag输入标志说明 9.4. c_oflag输出标志说明 9.5. c_cflag控制模式标志说明 9.6. c_cc[]控制字符说明 9.7. c_lflag本地模式标志说明 9.8. 下面介绍一些常用串口属性的设置方法。使用open()函数打开串口,open()函数有两个参数,第一个是要打开的设备名(如:/dev/ttyS0)。第二个是打开的方式。打开方式有以下三种:
-
O_RDWR,表示以读写方式打开串口。
-
O_NOCTTY,表示不成为端口的控制终端,如果没有这个选项,则任何输入(键盘按键)都会中断程序的执行。
-
O_NDELAY,表示程序不会关注DCD信号线所处的状态,即不管对端设备是运行或挂起。如果没有该选项,则程序会被设置成睡眠状态,直到DCD信号为低为止。
成功打开串口则会返回文件描述符,打开失败则返回-1。下面是一个打开串口的示例:
fd = open("/dev/ttyS0",O_RDWR|O_NDELAY|O_NDELAY);使用close()关闭打开的串口,唯一的参数是打开串口的文件描述符。下面是一个关闭串口的示例:
close(fd); //fd是打开串口返回的文件描述符用write()函数向串口写数据。下面是一个向串口写数据的示例:
n = write(fd,buff,len);/* n表示成功写到串口的字节数,如果写入失败则返回-1
fd是打开串口返回的文件描述符
buff表示写入的内容
len表示写入信息的长度。
*/
用read()函数从串口读取数据。下面是一个从串口读数据的示例:
n = read(fd,buff,len);/* n表示从串口读到字节数
fd是文件描述符
buff是读入字节存放的缓冲区
len表示读入的字节数
*/
通过fcntl()函数可以操作文件描述符,用以控制读取数据的状态。fcntl(fd,F_SETFL,0)表示没有数据则阻塞,处于等待状态,直到有数据到来;fcntl(fd,F_SETFL,FNDELAY)表示当端口没有数据时马上返回0。
所有的串口属性都在一个名为termios的结构体中,要使用该结构体要包含termios.h头文件。在该头文件中还定义两个重要的函数tcgetattr()和tcsetattr(),分别用以获取和设置串口的属性。如:tcgetattr(fd,&old_termios),tcsetattr(fd,TCSANOW,&new_termios)。old_termios是旧的串口属性,new_termios是重新设置的新串口属性。tcsetattr()函数中常量的意义是:
-
TCSANOW表示新设置的串口属性马上生效。
-
TCSADRAIN表示等所有数据传送完成后才生效。
-
TCSAFLUSH表示马上清空输入和输出缓存,然后应用新的串口设置。
termios结构体内容:
成员 描述-------------------------------------------
c_cflag 控制模式标志
c_lflag 本地模式标志
c_iflag 输入模式标志
c_oflag 输出模式标志
c_line line discipline
c_cc[NCCS] 控制字符
c_ispeed 输入波特率
c_ospeed 输出波特率
在termios结构中的四个标志控制了输入输出的四个不同部份。输入模式标志c_iflag决定如何解释和处理接收的字符。输出模式标志c_oflag决定如何解释和处理发送到tty设备的字符。控制模式标志决定设备的一系列协议特征,这一标志只对物理设备有效。本地模式标志c_lflag决定字符在输出前如何收集和处理。
在串口传输中,用波特率来表示传输的速度,1波特表示在1秒钟内可以传输1个码元。波特率设置可以使用cfsetispeed(&new_termios,B19200)和cfsetospeed(&new_termios,B19200)这两个函数来完成,默认的波特率为9600baud。cfsetispeed()函数用来设置输入的波特率,cfsetospeed()函数用来设置输出的波特率。B19200是termios.h头文件里定义的一个宏,表示19200的波特率。
CLOCAL和CREAD是c_cflag成员中与速率相关的标志,在串口编程中,这两个标志一定要有效,以确保程序在突发的作业控制或挂起时,不会成为端口的占有都,同时串口的接收驱动会自动读入数据。设置方法如下:
termios_new.c_cflag |= CLOCAL; //保证程序不会成为端的占有者termios_new.c_cflag |= CREAD; //使端口能读取输入的数据
设置串口属性不能直接赋值,要通过对termios不同成员进行"与"和"或"操作来实现。在termios.h文件,定义了各种常量,如上面介绍的CLOCAL,CREAD。这些常量的值是掩码,通过把这些常量与termios结构成员进行逻辑操作就可实现串口属性的设置。在编程时用"|="来启用属性,用"&=~"来取消属性。
-
BRKINT和IGNBRK
如果设置了IGNBRK,中断条件被忽略。如果没有设置IGNBRK而设置了BRKINT,中断条件清空输入输出队列中所有的数据并且向tty的前台进程组中所有进程发送一个SIGINT信号。如果这两个都没有设置,中断条件会被看作一个0字符。这时,如果设置了PARMRK,当检测到一个帧误差时将会向应用程序发送三个字节'\377''\0''\0',而不是只发送一个'\0'。
-
PARMRK和IGNPAR
如果设定了IGNPAR,则忽略接收到的数据的奇偶检验错误或帧错误(除了前面提到的中断条件)。如果没有设置IGNPAR而设置了PARMRK,当接收到的字节存在奇偶检验错误或帧错误的时候。将向应用程序发送一个三字节的'\377''\0''\n'错误报告。其中n表示所接收到的字节。如果两者都没有设置,除了接收到的字节存在奇偶检验错误或帧误差之外的中止条件都会向应用程序发送一个单字节('\0')的报告。
-
INPCK
如果设置,则进行奇偶校验。如果不进行奇偶检验,PARMRK和IGNPAR将对存在的奇偶校验错误不产生任何的影响。
-
ISTRIP
如果设置,所接收到的所有字节的高位将会被去除,保证它们是一个7位的字符。
-
INLCR
如果设置,所接收到的换行字符('\n')将会被转换成回车符('\r')。
-
IGNCR
如果设置,则会忽略所有接收的回车符('\r')。
-
ICRNL
如果设置,但IGNCR没有设置,接收到的回车符向应用程序发送时会变换成换行符。
-
IUCLC
如果IUCLC和IEXTEN都设置,接收到的所有大写字母发送给应程序时都被转换成小写字母。POSIX中没有定义该标记。
-
IXOFF
如果设置,为避免tty设备的输入缓冲区溢出,tty设备可以向终端发送停止符^S和开始符^Q,要求终端停止或重新开始向计算机发送数据。通过停止符和开始符来控制数据流的方式叫软件流控制,软件流控制方式较少用,我们主要还是用硬件流控制方式。硬件流控制在c_cflag标志中设置。
-
IXON
如果设置,接收到^S后会停止向这个tty设备输出,接收到^Q后会恢复输出。
-
IXANY
如果设置,则接到任何字符都会重新开始输出,而不仅仅是^Q字符。
-
IMAXBEL
如果设置,当输入缓冲区空间满时,再接收到的任何字符就会发出警报符'\a'。POSIX中没有定义该标记。
OPOST是POSIX定义的唯一一个标志,只有设置了该标志后,其它非POSIX的输出标记才会生效。
-
OPOST
开启该标记,后面的输出标记才会生效。否则,不会对输出数据进行处理。
-
OLCUC
如果设置,大写字母被转换成小写字母输出。
-
ONLCR
如果设置,在发送换行符('\n')前先发送回车符('\r')。
-
ONOCR
如果设置,当current column为0时,回车符不会被发送也不会被处理。
-
OCRNL
如果设置,回车符会被转换成换行符。另外,如果设置了ONLRET,则current column会被设为0.
-
ONLRET
如果设置,当一个换行符或回车符被发送的时候,current column会被设置为0。
-
OXTABS
如果设置,制表符会被转换成空格符。
-
CLOCAL
如果设置,modem的控制线将会被忽略。如果没有设置,则open()函数会阻塞直到载波检测线宣告modem处于摘机状态为止。
-
CREAD
只有设置了才能接收字符,该标记是一定要设置的。
-
CSIZE
设置传输字符的位数。CS5表示每个字符5位,CS6表示每个字符6位,CS7表示每个字符7位,CS8表示每个字符8位。
-
CSTOPB
设置停止位的位数,如果设置,则会在每帧后产生两个停止位,如果没有设置,则产生一个停止位。一般都是使用一位停止位。需要两位停止位的设备已过时了。
-
HUPCL
如果设置,当设备最后打开的文件描述符关闭时,串口上的DTR和RTS线会减弱信号,通知Modem挂断。也就是说,当一个用户通过Modem拔号登录系统,然后注销,这时Modem会自动挂断。
-
PARENB和PARODD
如果设置PARENB,会产生一个奇偶检验位。如果没有设置PARODD,则产生偶校验位,如果设置了PARODD,则产生奇校验位。如果没有设置PARENB,则PARODD的设置会被忽略。
-
CRTSCTS
使用硬件流控制。在高速(19200bps或更高)传输时,使用软件流控制会使效率降低,这个时候必须使用硬件流控制。
只有在本地模式标志c_lflag中设置了IEXITEN时,POSIX没有定义的控制字符才能在Linux中使用。每个控制字符都对应一个按键组合(^C、^H等),但VMIN和VTIME这两个控制字符除外,它们不对应控制符。这两个控制字符只在原始模式下才有效。
-
c_cc[VINTR]
默认对应的控制符是^C,作用是清空输入和输出队列的数据并且向tty设备的前台进程组中的每一个程序发送一个SIGINT信号,对SIGINT信号没有定义处理程序的进程会马上退出。
-
c_cc[VQUIT]
默认对应的控制符是^\,作用是清空输入和输出队列的数据并向tty设备的前台进程组中的每一个程序发送一个SIGQUIT信号,对SIGQUIT信号没有定义处理程序的进程会马上退出。
-
c_cc[verase]
默认对应的控制符是^H或^?,作用是在标准模式下,删除本行前一个字符,该字符在原始模式下没有作用。
-
c_cc[VKILL]
默认对应的控制符是^U,在标准模式下,删除整行字符,该字符在原始模式下没有作用。
-
c_cc[VEOF]
默认对应的控制符是^D,在标准模式下,使用read()返回0,标志一个文件结束。
-
c_cc[VSTOP]
默认对应的控制字符是^S,作用是使用tty设备暂停输出直到接收到VSTART控制字符。或者,如果设备了IXANY,则等收到任何字符就开始输出。
-
c_cc[VSTART]
默认对应的控制字符是^Q,作用是重新开始被暂停的tty设备的输出。
-
c_cc[VSUSP]
默认对应的控制字符是^Z,使当前的前台进程接收到一个SIGTSTP信号。
-
c_cc[VEOL]和c_cc[VEOL2]
在标准模式下,这两个下标在行的末尾加上一个换行符('\n'),标志一个行的结束,从而使用缓冲区中的数据被发送,并开始新的一行。POSIX中没有定义VEOL2。
-
c_cc[VREPRINT]
默认对应的控制符是^R,在标准模式下,如果设置了本地模式标志ECHO,使用VERPRINT对应的控制符和换行符在本地显示,并且重新打印当前缓冲区中的字符。POSIX中没有定义VERPRINT。
-
c_cc[VWERASE]
默认对应的控制字符是^W,在标准模式下,删除缓冲区末端的所有空格符,然后删除与之相邻的非空格符,从而起到在一行中删除前一个单词的效果。POSIX中没有定义VWERASE。
-
c_cc[VLNEXT]
默认对应的控制符是^V,作用是让下一个字符原封不动地进入缓冲区。如果要让^V字符进入缓冲区,需要按两下^V。POSIX中没有定义VLNEXT。
要禁用某个控制字符,只需把它设置为_POSIX_VDISABLE即可。但该常量只在Linux中有效,所以如果程序要考虑移植性的问题,请不要使用该常量。
-
ICANON
如果设置,则启动标准模式,如果没有设置,则启动原始模式。
-
ECHO
如果设置,则启动本地回显。如果没有设置,则除了ECHONL之外,其他以ECHO开头的标记都会失效。
-
ECHOCTL
如果设置,则以^C的形式打印控制字符,如:按Ctrl+C显示^C,按Ctrl+?显示^?。
-
ECHOE
如果在标准模式下设定了ECHOE标志,则当收到一个ERASE控制符时将删除前一个显示字符。
-
ECHOK和ECHOKE
在标准模式下,当接收到一个KILL控制符,则在缓冲区中删除当前行。如果ECHOK、ECHOKE和ECHOE都没有设置,则用ECHOCTL表示的KILL字符(^U)将会在输出终端上显示,表示当前行已经被删除。
如果已经设置了ECHOE和ECHOK,但没有设置ECHOKE,将会在输出终端显示ECHOCTL表示的KILL字符,紧接着是换行,如果设置了OPOST,将会通过OPOST处理程序进行适当的处理。
如果ECHOK、ECHOKE和ECHOE都有设置,则会删除当前行。
在POSIX中没有定义ECHOKE标记,在没有定义ECHOKE标记的系统中,设置ECHOK则表示同时设置了ECHOKE标志。
-
ECHONL
如果在标准模式下设置了该标志,即使没有设置ECHO标志,换行符还是会被显示出来。
-
ECHOPRT
如果设置,则字符会被简单地打印出来,包括各种控制字符。在POSIX中没有定义该标志。
-
ISIG
如果设置,与INTR、QUIT和SUSP相对应的信号SIGINT、SIGQUIT和SIGTSTP会发送到tty设备的前台进程组中的所有进程。
-
NOFLSH
一般情况下,当接收到INTR或QUIT控制符的时候会清空输入输出队列,当接收到SUSP控制符时会清空输入队列。但是如果设置了NOFLUSH标志,则所有队列都不会被清空。
-
TOSTOP
如果设置,则当一个非前台进程组的进程试图向它的控制终端写入数据时,信号SIGTTOU会被被发送到这个进程所在的进程组。默认情况下,这个信号会使进程停止,就像收到SUSP控制符一样。
-
IEXIEN
默认已设置,我们不应修改它。在Linux中IUCLC和几个与删除字符相关的标记都要求在设置了IEXIEN才能正常工作。
-
设置流控制
termios_new.c_cflag &= ~CRTSCTS; //不使用流控制
termios_new.c_cflag |= CRTSCTS; //使用硬件流控制
termios_new.c_iflag |= IXON|IXOFF|IXANY; //使用软件流控制
-
屏蔽字符大小位
termios_new.c_cflag &= ~CSIZE;
-
设置数据位大小
termios_new.c_cflag |= CS8; //使用8位数据位
termios_new.c_cflag |= CS7; //使用7位数据位
termios_new.c_cflag |= CS6; //使用6位数据位
termios_new.c_cflag |= CS5; //使用5位数据位
-
设置奇偶校验方式
termios_new.c_cflag &= ~PARENB; //无奇偶校验
termios_new.c_cflag |= PARENB; //奇校验
termios_new.c_cflag &= ~PARODD;
termios_new.c_cflag |= PARENB; //偶校验
termios_new.c_cflag &= ~PARODD;
-
停止位
termios_new.c_cflag |= CSTOPB; //2位停止位
termios_new.c_cflag &= ~CSTOPB; //1位停止位
-
输出模式
termios_new.c_cflag &= ~OPOST; //原始数据(RAW)输出
-
控制字符
termios_new.c_cc[VMIN] = 1; //读取字符的最小数量
termios_new.c_cc[VTIME] = 1; //读取第一个字符的等待时间
-
关闭终端回显,键盘输入的字符不会在终端窗口显示。
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
int main(void)
{
struct termios ts,ots;
char passbuf[1024];
tcgetattr(STDIN_FILENO,&ts); /* STDIN_FILENO的值是1,表示标准输入的文件描述符 */
ots = ts;
ts.c_lflag &= ~ECHO; /* 关闭回终端回显功能*/
ts.c_lflag |= ECHONL;
tcsetattr(STDIN_FILENO,TCSAFLUSH,&ts); /* 应用新终端设置 */
fgets(passbuf,1024,stdin); /* 输入字符不会在终端显示 */
printf("you input character = %s\n",passbuf);
tcsetattr(STDIN_FILENO,TCSANOW,&ots); /* 恢复旧的终端设备 */
}
Table of Contents
10.1. 内核漏洞介绍Linux内核以稳定和安全著称,但随着Linux使用范围的不断扩展,各种漏洞也慢慢被内核开发人员或黑客发现。这里介绍有关Linux内核和基于Linux的开源软件的安全问题。
在实际解决问题的时候,各种数据都不是孤立的,数据之间总是存在关系,这种数据之间的关系叫做数据结构。我们可以把数据结构的形式归并为四种:
-
集合:数据之间没有对应关系,但同属于一个集合。如汽车是一个集合,编程语言也是一个集合。
-
线性结构:各数据有一一对应的关系,有前驱也有后续。
-
树形结构:各数据间存在一对多的关系,有一个前驱但有多个后续。
-
图:各数据间有多对多的关系,对前驱和后续没有限制。
数据类型是一个值的集合和定义在这个值集上的一组操作的总称。
数据类型可分两类,一类是每个对象仅由单值组成,称为原子类型,如整型、字符型等。另一类是由某种结构组成的类型,叫结构类型,如数组、字符串等。
抽象数据结构(Abstract Data Type,ADT)是一种数据类型及在这个类型上定义的一组合法的操作。
算法(Algorithm)是一个有穷规则(或语句、指令)的有序集合。通俗地说,就是计算机解决问题的过程。算法应具备以下几个重要的特性:
-
输入:一个算法有零个或多个输入。
-
输出:一个算法至少有一个输出,这种输出是同输入有着某些特定关系的量。没有输出的算法是没有意义的。
-
有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。
-
确定性:算法中每条指令的含义都必须明确,无二义性。对相同的输入,必须有相同的结果。
-
可行性:算法中的每条指令的执行时间都是有限的。
描述算法的工具:自然语言、流程图、形式化语言和程序设计语言。
由瑞士科学家Niklaus Wirthrn提出的计算机界公认的公式:算法 + 数据结构 = 程序
算法设计的要求:正确、可读、健壮、快速、节省存储空间。
线性结构中的数据元素之间是一种线性关系,数据元素一个接一个地排列。如排除的队列、表格中一行行的记录等。数据元素可以包含多个数据项(字段),包含多个数据项的数据元素叫做记录。由大量记录组成的线性表又称为文件。
线性表的数学表示模型:a0,a1,a2,...a(n-1)。
顺序连续存放的线性表是最简单的,称为顺序存储结构线性表。它在内存开辟一片连续的存储空间,让线性表的第一个元素存放在内存空间的第一个位置,第二个元素存放在第二个位置,其它元素以此类推。数据元素间的前驱和后继关系表现在存放位置的前后关系上。顺序存储结构线性表算法在插入或删除操作时的效率不高。平均起来,每插入或删除一个元素需要移动一半的元素,最坏的情况更要移动全部的元素。另外,顺序表不利于存储空间的分配。在经常需要进入插入或删除操作的线性表中,使用顺序存储结构线性表是不合适的。所以我们有了链式存储结构线性表。
数组就是顺序存储结构的程序实现。 |
链式存储结构线性表由结点组成,每个节点由一个数据元素和一个指向下个结点的指针组成。每个结点中如果只有一个指向后续指针的链表,叫单链表。由于链表通过指针指向下一个结点,所以数据元素可以分散存储。
单链表的建立是一种动态内在管理操作,表中的每个节点占用的存储空间无需预先指定,而是在运行时动态申请。
单链表一旦创建就可对链表进行操作。
-
查找值为x的节点,并返回该节点地址。算法分析:从单链表的第一个节点开始,判断当前节点的数据域的值是否为x,若是,则返回该节点的指针域,否则,依据指针域内的指针查找下一节点,直至表结束。若找不到,则返回空。
-
查找第i个节点,返回期指针。算法分析:从单链表的第一个节点开始,依次判断当前节点是否为第i个节点,若是则返回其指针,否则,依据指针域内的指针查找下一节点,直至表结束。若找不到,则返回空。
EthernetII帧的结构(DMAC+SMAC+Type+Data+CRC),EthernetII帧的大小是有限制的,最小不能小于64字节,最大不能超过1518字节,否则帧会被丢弃。一个EthernetII帧包括的内容有:
-
DMAC,目的MAC地址,占48个bit,共6个字节。
-
SMAC,源MAC地址,占48个bit,共6个字节。
-
Type,帧类型,如ip,arp等。占16个bit,共2个字节。
-
Data,帧数据,容量是变化的,但最大不能越过1500个字节,最小不能小于46个字节。
-
CRC,校验码,占32个bit,共4个字节。
IP包结构:
下面我们开发一个模拟Echo服务功能的tcp程序。通过这个简单的程序我们可以学习tcp/ip网络编程的基础结构。
tcpserver.c是服务端程序,运行后会监听一个端口。
debian:~/c/kernelmodule# cat tcpserver.c#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <errno.h>
int main(int argc,char *argv[]){
int iswork,data,fd1,fd2;
pid_t pidchild;
socklen_t clientlen;
struct sockaddr_in clientaddr;
struct sockaddr_in serveraddr;
char buffer[1000];
if(argc != 2){
printf("Usage: tcpserver [port number]\n");
exit(1);
}
if((fd1 = socket(AF_INET,SOCK_STREAM,0))<0){
printf("socket error!\n");
exit(1);
}
memset(&serveraddr,0,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(atoi(argv[1]));
if(bind(fd1,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0){
printf("bind error!\n");
exit(1);
}
if(listen(fd1,3)<0){
printf("listen error!\n");
exit(1);
}
iswork = 1;
while(iswork){
clientlen = sizeof(clientaddr);
if((fd2 = accept(fd1,(struct sockaddr *)&clientaddr,&clientlen))<0){
printf("accept error!\n");
exit(1);
}
if((pidchild = fork()) == -1){
printf("fork error!\n");
exit(1);
}
if(pidchild == 0){
if(close(fd1) == -1){
printf("close error!\n");
exit(1);
}
printf("Connect from %s\n",inet_ntoa(clientaddr.sin_addr));
while(1){
memset(buffer,0,1000);
if(data = read(fd2,buffer,sizeof(buffer))>0){
printf("%s",buffer);
if(write(fd2,buffer,sizeof(buffer))<0){
printf("send error!\n");
exit(1);
}
}
}
exit(0);
}
if(close(fd2) == -1){
printf("close error!\n");
exit(1);
}
}
}
几个主要函数说明:
-
socket()函数,创建套接口,返回套接口句柄。
-
bind()函数,
-
listen()函数,
-
htonl()和htons()
-
accept()
数据包在应用层称为data,在TCP层称为segment,在IP层称为packet,在数据链路层称为frame。
在80x86微处理器中,有三种存储器地址:
-
逻辑地址(logical address),包含在机器语言指令中用来指定一个操作数或一条指令的地址。每个逻辑地址都由一个段(segment)和一个偏移量(offset)组成。偏移量指明了从段的开始到实际地址之间的距离。
-
线性地址(linear address)(也称为虚拟地址,virtual address),它是一个32位无符号整数,可用以表达高达4G的地址(2的32次方)。通常以十六进制数表示,值的范围从0X00000000到0Xffffffff。
-
物理地址(physical address),用于存储器芯片级存储单元寻址,它们与从微处理器的地址引脚发送到存储器总线上的电信号相对应。物理地址由32位无符号整数表示。
CPU控制单元通过一种称为分段单元(segmentation unit)的硬件电路把一个逻辑地址转换成线性地址;线性地址又通过一个分页单元(paging unit)的硬件电路把一个线性地址转换成物理地址。
逻辑地址由两部份组成,一个段标识符和一个指定段由相对地址的偏移量。段标识符是一个16位长的字段,称为段选择符(segment selector),偏移量是一个32位长的字段。
处理器提供专门的段寄存器以快速处理段选择符,段寄存器的唯一目的就是存放段选择符。共有6个段寄存器,分别是cs、ss、ds、es、fs和gs。其中cs、ss、ds寄存器有专门的用途。
-
cs是代码段寄存器,指向包含程序指令的段。
-
ss是栈寄存器,指向包含当前程序栈的段。
-
ds是数据段寄存器,指向包含静态数据或者外部数据的段。
cs寄存器有一个重要功能,它包含有一个两位的字段,用以指明CPU当前特权级别(Current Privilege Level,CPL)。值0表示最高优先级,值3表示最低优先级。Linux只用到0级和3级,分别表示内核态和用户态。
每个段由一个8字节的段描述符表示,它描述了段的特征。段描述符放在全局描述符表(Global Descriptor Table,GDT)中或局部描述符表(Local Descriptor Table,LDT)中。
段描述符的组成:
-
32位的Base字段,含有段的第一个字节的线性地址。
-
粒度标记G。如果该位清0,则段大小以字节为单位,否则以4096字节的倍数计。
-
20位的Limit字段指定段的长度(以字节为单位,Limit字段为0的段被认为是空段)。当G为0时,段的大小在1字节到1MB之间;否则段的大小在4KB到4GB之间。
-
系统标记S。如果它被清0,则这是一个系统段,用于存储内核数据结构,否则,它是一个普通的代码段或数据段。
-
4位Type字段,描述段的类型和它的访问权限。常用的Type有以下几种:
-
代码段描述符
-
数据段描述符
-
任务状态段描述符
-
局部描述符表描述符
-
在编程思路上,机制表示需要提供什么功能,策略表示如何使用这些功能。区分机制和策略是UNIX设计最重要和最好的思想之一。如X系统就由X服务器和X客户端组成。X服务器实现机制,负责操作硬件,给用户程序提供一个统一的接口。而X客户端实现策略,负责如何使用X服务器提供的功能。设备驱动程序也是机制与策略分离的典型应用。在编写硬件驱动程序时,不要强加任何特定的策略。
Linux系统将设备分成三种类型,分别是字符设备、块设备和网络接口设备。
在linux中通过设备文件访问硬件,设备文件位于/dev目录下。设备文件是一种信息文件,普通文件的目的在于存储数据,设备文件的目的在于向内核提供控制硬件的设备驱动程序的信息。设备文件保存了多种信息,其中重要的有设备类型信息,主设备号(major),次设备号(minor)。主设备号与次设备号起到连接应用程序和设备驱动程序的作用。当应用程序利用open()函数打开设备文件时,内核从相应的设备文件中得到主设备号,从而查找到相应的设备驱动程序,由次设备号查找实际设备。所以主设备号对应设备驱动程序,次设备号对应由该驱动程序所驱动的实际设备。通过设备文件可以向硬件传送数据,也可从硬件接收数据。
设备文件使用mknod命令生成。mknod命令语法如下:
mknod [设备文件名] [设备文件类型] [主设备号] [次设备号]字符设备用c表示,块设备用b表示,网络设备没有专门的设备文件。
读写设备文件时要使用低级输入输出函数,不要使用带缓冲的以f开头的流文件输入输出函数。但并不是所有低级输入输出函数都可以用在设备文件上,可以用在设备文件的低级输入输出函数有以下几个:
open() 打开文件或设备close() 关闭文件
read() 读取数据
write() 写数据
lseek() 改变文件的读写位置
ioctl() 实现read(),write()外的特殊控制,该函数只在设备文件中使用
fsync() 实现写入文件上的数据和实际硬件的同步
字符设备在系统中以设备文件的形式表示,位于/dev目录下。每个字符设备都有一个主设备号和次设备号,主设备号标识设备对应的驱动程序,次设备号标识设备文件所指的具体设备。
主次设备号的数据类型是dev_t,在/linux/types.h中定义。在2.6内核中,dev_t是一个32位的数,其中12位用来表示主设备号,其余20位用来表示次设备号。要获得设备的主次设备号可以使用内核提供的宏:
MAJOR(dev_t dev); #获得主设备号MINOR(dev_t dev); #获得次设备号
这些宏定义位于linux/kdev_t.h中。如果要把主次设备号转换成dev_t类型,则可使用:
MKDEV(int major, int minor);在建立一个字符设备之前,需要为它分配一个或多个设备号。使用register_chrdev_region()函数完成设备号的分配。该函数在linux/fs.h中声明。原型如下:
int register_chrdev_region(dev_t first, unsigned int count, char *name);first:是要分配的主设备号范围的起始值,次设备号一般设置为0;
count:是所请求的连续设备号的个数;
name:是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。
如果分配成功则返回0,分配失败则返回一个负的错误码,所请求的设备号无效。
还有一个自动分配设备号的函数alloc_chrdev_region(),原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);dev: 自动分配到设备号范围中的第一个主设备号;
firstminor:自动分配的第一个次设备号,通常为0;
count: 是所请求的连续设备号的个数;
name: 是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。
如果我们不再使用设备号,则要使用unregister_chrdev_region()函数释放它。函数原型如下:
void unregister_chrdev_region(dev_t first, unsigned int count);函数的参数作用同上
我们一般在模块的清除函数中调用设备号释放函数。
在内核源码目录的Documentation/devices.txt文件中列出了已静态分配给常用设备的主设备号。为了减少设备号分配的冲突,我们一般要使用alloc_chrdev_region()函数来自动分配设备号。
文件操作结构:structfile_operations,在linux/fs.h中定义。它包含一组函数指针,实现文件操作的系统调用,如read、write等。每个打开的文件都和一个文件操作结构关联(通过file结构中指向file_operations结构的f_op字段进行关联)。
文件结构:struct file,在linux/fs.h中定义。file结构代表一个打开的文件,由内核在open时创建。指向文件结构的指针在内核中通常称为filp(文件指针)。当文件的所有实例都被关闭之后,内核会释放这个数据结构。
节点结构:struct inode,在linux/fs.h中定义。inode结构是内核表示文件的方法,而file结构是以文件描述符的方式表示文件的方法。结构中以下两个字段对编写驱动程序有用:
-
dev_t i_rdev,该字段包含了真正的设备编号。
-
struct cdev *i_cdev,该字段包含指向struct cdev结构的指针。
从设备的inode获取主次设备号的宏:
unsigned int iminor(struct inode *inode);unsigned int imajor(struct inode *inode);
下面两个是字符设备读写操作最重要的内核函数。
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n);读操作,把数据从内核空间复制到用户空间,返回不能复制的字节数,如果成功则返回0。
to 目的地址,在用户空间中;
from 源地址,在用户空间;
n 要复制的字节数。
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n);
写操作,把数据从用户空间复制到内核空间,返回不能复制的字节数,如果成功则返回0。
to 目的地址,在内核空间中;
from 源地址,在用户空间;
n 要复制的字节数。
pci设备上电时,硬件保持未激活状态。设备不会有内存和I/O端口映射到计算机的地址空间。每个PCI主板上都配备有能够处理PCI的BIOS、NVRAM或PROM等固件。这些固件通过读写PCI控制器中的寄存器,提供了对设备配置地址空间的访问。系统引导时,固件在每个PCI设备上执行配置事务,以便为它提供的每个地址区域分配一个安全的位置。当驱动程序访问设备时,它的内存和I/O区域已经被映射到了处理器的地址空间。
所有PCI设备都有至少256字节的地址空间。前64字节是标准化的,每种设备都有且意义相同,其余字节是设备相关的。
在内核中有三个主要的数据结构与PCI接口有关,在开发PCI设备驱动程序时要用到,分别是:
-
pci_device_id,PCI设备类型的标识符。在include/linux/mod_devicetable.h头文件中定义。
struct pci_device_id {
__u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/
__u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */
__u32 class, class_mask; /* (class,subclass,prog-if) triplet */
kernel_ulong_t driver_data; /* Data private to the driver */
};
PCI设备的vendor、device和class的值都是预先定义好的,通过这些参数可以唯一确定设备厂商和设备类型。这些PCI设备的标准值在include/linux/pci_ids.h头文件中定义。
pci_device_id需要导出到用户空间,使模块装载系统在装载模块时知道什么模块对应什么硬件设备。宏MODULE_DEVICE_TABLE()完成该工作。
设备id一般用数组形式。如:
static struct pci_device_id rtl8139_pci_tbl[] = {
{0x10ec, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
....
};
MODULE_DEVICE_TABLE (pci, rtl8139_pci_tbl);
-
pci_dev,标识具体的PCI设备实例,与net_device类似。内核通过该内核结构来访问具体的PCI设备。在include/linux/pci.h头文件中定义。
-
pci_driver,设备驱动程序数据结构,它是驱动程序与PCI总线的接口,有大量的回调函数和指针,向PCI核心描述了PCI驱动程序。在include/linux/pci.h头文件中定义。
static struct pci_driver rtl8139_pci_driver = {
.name = DRV_NAME, #设备名
.id_table = rtl8139_pci_tbl, #pci设备的id表组
.probe = rtl8139_init_one, #初始化函数
.remove = __devexit_p(rtl8139_remove_one), #退出函数
#ifdef CONFIG_PM #如果设备支持电源管理
.suspend = rtl8139_suspend, #休眠
.resume = rtl8139_resume, #从休眠恢复
#endif /* CONFIG_PM */
};
内核通过pci_register_driver和pci_unregister_driver函数来注册和注消PCI设备驱动程序。这两个函数在drivers/pci/pci.c源码中定义。pci_register_driver函数需要使用pci_driver数据结构作为参数。通过注册,PCI设备就与PCI设备驱动程序关联起来了。
PCI设备最大的优点是可以自动探测每个设备所需的IRQ和其它资源。有两种探测方式,一种是静态探测,一种是动态探测。静态探测是通过设备驱动程序自动选择相关资源,动态探测是指支持热插拔设备的功能。
PCI设备通过pci_driver结构中的suspend和resume函数指针支持电源管理。可实现暂停和重新启动PCI设备的功能。
/lib/modules/KERNEL_VERSION/modules.pcimap文件列出内核所支持的所有PCI设备和它们的模块名。
debian:/lib/modules/2.6.23.9# cat modules.pcimap | more# pci module vendor device subvendor subdevice class class_mask driver_data
snd-trident 0x00001023 0x00002000 0xffffffff 0xffffffff 0x00040100 0x00ffff00 0x0
snd-trident 0x00001023 0x00002001 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0
...
8139cp 0x000010ec 0x00008139 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0
8139cp 0x00000357 0x0000000a 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0
...
内核使用了大量不同的宏来标记具有不同作用的函数和数据结构。如宏__init、__devinit等。这些宏在include/linux/init.h头文件中定义。编译器通过这些宏可以把代码优化放到合适的内存位置,以减少内存占用和提高内核效率。
下面是一些常用的宏:
-
__init,标记内核启动时使用的初始化代码,内核启动完成后不再需要。以此标记的代码位于.init.text内存区域。它的宏定义是这样的:
#define _ _init _ _attribute_ _ ((_ _section_ _ (".text.init")))
-
__exit,标记退出代码,对于非模块无效。
-
__initdata,标记内核启动时使用的初始化数据结构,内核启动完成后不再需要。以此标记的代码位于.init.data内存区域。
-
__devinit,标记设备初始化使用的代码。
-
__devinitdata,标记初始化设备数据结构的函数。
-
__devexit,标记移除设备时使用的代码。
-
xxx_initcall,一系列的初始化代码,按降序优先级排列。
_init_begin -------------------
| .init.text | ---- __init
|-------------------|
| .init.data | ---- __initdata
_setup_start |-------------------|
| .init.setup | ---- __setup_param
__initcall_start |-------------------|
| .initcall1.init | ---- core_initcall
|-------------------|
| .initcall2.init | ---- postcore_initcall
|-------------------|
| .initcall3.init | ---- arch_initcall
|-------------------|
| .initcall4.init | ---- subsys_initcall
|-------------------|
| .initcall5.init | ---- fs_initcall
|-------------------|
| .initcall6.init | ---- device_initcall
|-------------------|
| .initcall7.init | ---- late_initcall
__initcall_end |-------------------|
| |
| ... ... ... |
| |
__init_end -------------------
初始化代码的特点是:在系统启动运行,且一旦运行后马上退出内存,不再占用内存。
对于驱动程序模块来说,这些优化标记使用的情况如下:
-
通过module_init()和module_exit()函数调用的函数就需要使用__init和__exit宏来标记。
-
pci_driver数据结构不需标记。
-
probe()和remove()函数应该使用__devinit和__devexit标记,且只能标记probe()和remove()
-
如果remove()使用__devexit标记,则在pci_driver结构中要用__devexit_p(remove)来引用remove()函数。
-
如果你不确定需不需要添加优化宏则不要添加。
内核通过不同的接口向用户输出内核信息。我们可通过这些接口访问和修改内核参数。共有三种接口,其中两种是procfs和sysfs虚拟文件系统,第三种是sysctl命令。
-
启用procfs虚拟文件系统的内核选项是"Filesystems-->Pseudo filesystems-->proc filesystem support"。procfs文件系统挂载在/proc目录,可用cat、more等shell命令查看目录中的文件。
-
sysctl命令也可以修改和查看内核变量,sysctl操作的内核变量位于/proc/sys目录下。启用sysctl支持的内核选项是"General setup-->Sysctl support"。
-
procfs和sysctl接口已使用多年,从2.6内核开始,引入新的sysfs虚拟文件系统,它挂载在/sys目录下。启用sysfs的内核选项是"Filesystems-->Pseudo filesystems-->sysfs filesystem support(NEW)"。sysfs以更整齐更直观的方式向用户展示了内核的各种参数。/proc将会向sysfs迁移。
另外,通过ioctl(input/output control)systemcall和Netlink接口也可以向内核发送命令,执行内核参数配置工作,大多数的网络配置参数都可以用这两个接口修改。ifconfig和route命令使用ioctl接口,IPROUTE2使用Netlink接口。
网络的ioctl命令在include/linux/sockios.h中定义。这些命令被定义成类似于SIOCSIFMTU的宏,宏的命令规则是这样的,开头四个字符SIOC代表ioctl命令;S表示set,G表示get;if表示接口类型;MTU表示mtu。其它字符的表示方式还有:ADD表示添加,RT表示路由等。
我们可以通过内核初始化选项,在系统启动时或内核模块加载时微调内核的功能。
模块的初始化选项是通过模块程序中的module_param宏传递的。如:
...module_param(multicast_filter_limit, int, 0444);
module_param(max_interrupt_work, int, 0444);
module_param(debug, int, 0444);
...
module_param宏的第一个参数是选项名,可在/sys虚拟文件系统中该模块的parameter目录中中查看到。第二个参数是选项类型,第三个参数是选项的值。上面的宏是sis900网卡的模块选项。在我的系统中显示为:
debian:/sys/module/sis900/parameters# ls -l总计 0
-r--r--r-- 1 root root 4096 2007-12-20 11:51 max_interrupt_work
-r--r--r-- 1 root root 4096 2007-12-20 11:51 multicast_filter_limit
-r--r--r-- 1 root root 4096 2007-12-20 11:51 sis900_debug
debian:/sys/module/sis900/parameters#
一个简单的hello world内核模块,基于2.6内核。
#include <linux/init.h>#include <linux/module.h>
MODULE_LICENSE("GPL");
static int hello_init(void)
{
printk(KERN_ALERT "hello, world\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodby,cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
上面的内核模块定义了两个函数,一个是module_init,该函数在内核模块加载时执行hello_init函数;另一个是module_exit,该函数在内核模块卸载时执行hello_exit函数。module_init()函数和module_exit()函数在include/linux/init.h头文件中声明。module_init()是模块的入口函数,当模块是编译进内核的话,则该函数会在系统启动时,被do_initcalls()函数调用来装入模块。当模块不编译进内核的话,则该函数会在加载模块时执行。
在编译内核模块前,要先写一个Makefile文件。
ifneq ($(KERNELRELEASE),)obj-m := HelloModule.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
正式编译。
debian:~/c/kernelmodule# makemake -C /lib/modules/2.6.17.1/build M=/root/c/kernelmodule modules
make[1]: Entering directory `/usr/src/linux-2.6.17.1'
Building modules, stage 2.
MODPOST
make[1]: Leaving directory `/usr/src/linux-2.6.17.1'
debian:~/c/kernelmodule#
编译完成后,在当前目录会生成HelloModule.ko内核模块,用insmod命令就可以装载内核模块。
debian:~/c/kernelmodule# insmod HelloModule.ko运行上面的命令后会直接返回命令行状态。用lsmod命令查看已加载的模块。
debian:~/c/kernelmodule# lsmodModule Size Used by
HelloModule 992 0
ppdev 6788 0
lp 8260 0
thermal 11016 0
...
用rmmod命令卸载模块。
debian:~/c/kernelmodule# rmmod HelloModule.ko通过查看系统内核日志,可以看到内核模块打印的信息。
debian:~/c/kernelmodule# vi /var/log/kern.log...
Nov 14 15:11:04 debian kernel: hello, world
Nov 14 15:16:43 debian kernel: Goodby,cruel world
...
注意:本机的gcc版本要和编译内核的gcc版本一致。否则内核模块在加载时会出现"insmod: error inserting 'HelloModule.ko': -1 Invalid module format"的错误提示。内核日志也有更细致的提示。
Nov 14 14:46:49 debian kernel: HelloModule: version magic '2.6.17.1 mod_unload PENTIUMIII gcc-4.2' should be '2.6.17.1 mod_unload PENTIUMIII gcc-3.3'在内核的API函数中,以双下划线开头(__)开头的函数是低层操作函数,在使用时要特别注意。
内核模块的描述信息包括模块作者,代码使用的许可协议等。添加方法是在源代码中使用一系列以MODULE_开头的宏。这些描述信息可以在shell环境下使用modinfo命令显示出来。
-
MODULE_AUTHOR("JIMS.YANG"),模块作者信息。
-
MODULE_DESCRIPTION("8139 NETWORK CARD DRIVER."),模块简单描述文本。
-
MODULE_LICENSE("GPL"),模块代码的许可协议。
-
MODULE_VERSION("xx.xx.xx"),模块的版本信息。
上面已介绍了查询、安装和删除内核模块的命令。Linux中与内核模块相关的命令还有几个,下面分别介绍一下。
-
modinfo,查询模块信息。
debian:~# modinfo snd
filename: /lib/modules/2.6.23.9/kernel/sound/core/snd.ko
author: Jaroslav Kysela <perex@suse.cz>
description: Advanced Linux Sound Architecture driver for soundcards.
license: GPL
alias: char-major-116-*
vermagic: 2.6.23.9 mod_unload PENTIUMIII
depends: soundcore
parm: cards_limit:Count of auto-loadable soundcards. (int)
parm: major:Major # for sound driver. (int)
-
modprobe,自动化的模块安装删除工具。配置文件位于/ect/modules.conf,该文件由update-modules工具自动生,不要手动去修改。对比insmod和rmmod命令,modprobe能自动处理模块间的依赖关系。在安装一个模块时,能自动安装该模块所需的其它模块。
-
depmod,生成内核模块依赖关系表modules.dep,位于/lib/modules/KERNEL_VERSION目录。该表被modprobe命令使用。
-
modconf,模块配置工具,它有一个GUI界面。
Table of Contents
21.1. sk_buff结构 21.2. sk_buff结构操作函数 21.3. net_device结构 21.4. 网络设备初始化 21.5. 网络设备与内核的沟通方式 21.6. 网络设备操作层的初始化 21.7. 内核模块加载器 21.8. 虚拟设备 21.9. 8139too.c源码分析 21.10. 内核网络数据流Linux强大的网络功能是如何实现的,让我们一起进入Linux内核的网络系统了解一下吧。
一些有用的文档资源:
-
网卡驱动程序目录:/usr/src/LINUX-KERNEL-VERSION/drivers/net/
-
一些网络相关的文档:/usr/src/linux/Documentation/networking
在Linux内核的网络实现中,使用了一个缓存结构(structsk_buff)来管理网络报文,这个缓存区也叫套接字缓存。sk_buff是内核网络子系统中最重要的一种数据结构,它贯穿网络报文收发的整个周期。该结构在内核源码的include/linux/skbuff.h文件中定义。我们有必要了解结构中每个字段的意义。
一个套接字缓存由两部份组成:
-
报文数据:存储实际需要通过网络发送和接收的数据。
-
管理数据(struct sk_buff):管理报文所需的数据,在sk_buff结构中有一个head指针指向内存中报文数据开始的位置,有一个data指针指向报文数据在内存中的具体地址。head和data之间申请有足够多的空间用来存放报文头信息。
struct sk_buff结构在内存中的结构示意图:
sk_buff----------------------------------- ------------> skb->head
| headroom |
|-----------------------------------| ------------> skb->data
| DATA |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|-----------------------------------| ------------> skb->tail
| tailroom |
----------------------------------- ------------> skb->end
内核通过alloc_skb()和dev_alloc_skb()为套接字缓存申请内存空间。这两个函数的定义位于net/core/skbuff.c文件内。通过这alloc_skb()申请的内存空间有两个,一个是存放实际报文数据的内存空间,通过kmalloc()函数申请;一个是sk_buff数据结构的内存空间,通过kmem_cache_alloc()函数申请。dev_alloc_skb()的功能与alloc_skb()类似,它只被驱动程序的中断所调用,与alloc_skb()比较只是申请的内存空间长度多了16个字节。
内核通过kfree_skb()和dev_kfree_skb()释放为套接字缓存申请的内存空间。dev_kfree_skb()被驱动程序使用,功能与kfree_skb()一样。当skb->users为1时kfree_skb()才会执行释放内存空间的动作,否则只会减少skb->users的值。skb->users为1表示已没有其他用户使用该缓存了。
skb_reserve()函数为skb_buff缓存结构预留足够的空间来存放各层网络协议的头信息。该函数在在skb缓存申请成功后,加载报文数据前执行。在执行skb_reserve()函数前,skb->head,skb->data和skb->tail指针的位置的一样的,都位于skb内存空间的开始位置。这部份空间叫做headroom。有效数据后的空间叫tailroom。skb_reserve的操作只是把skb->data和skb->tail指针向后移,但缓存总长不变。
运行skb_reserve()前sk_buff的结构sk_buff
---------------------- ----------> skb->head,skb->data,skb->tail
| |
| |
| |
| |
| |
| |
| |
| |
| |
--------------------- ----------> skb->end
运行skb_reserve()后sk_buff的结构
sk_buff
---------------------- ----------> skb->head
| |
| headroom |
| |
|--------------------- | ----------> skb->data,skb->tail
| |
| |
| |
| |
| |
--------------------- ----------> skb->end
skb_put()向后扩大数据区空间,tailroom空间减少,skb->data指针不变,skb->tail指针下移。
skb_push()向前扩大数据区空间,headroom空间减少,skb->tail指针不变,skb->data指针上移
skb_pull()缩小数据区空间,headroom空间增大,skb->data指针下移,skb->tail指针不变。
skb_shared_info结构位于skb->end后,用skb_shinfo函数申请内存空间。该结构主要用以描述data内存空间的信息。
--------------------- -----------> skb->head| |
| |
| sk_buff |
| |
| |
| |
|---------------------| -----------> skb->end
| |
| skb_share_info |
| |
---------------------
skb_clone和skb_copy可拷贝一个sk_buff结构,skb_clone方式是clone,只生成新的sk_buff内存区,不会生成新的data内存区,新sk_buff的skb->data指向旧data内存区。skb_copy方式是完全拷贝,生成新的sk_buff内存区和data内存区。。
net_device结构是Linux内核中所有网络设备的基础数据结构。包含网络适配器的硬件信息(中断、端口、驱动程序函数等)和高层网络协议的网络配置信息(IP地址、子网掩码等)。该结构的定义位于include/linux/netdevice.h
每个net_device结构表示一个网络设备,如eth0、eth1...。这些网络设备通过dev_base线性表链接起来。内核变量dev_base表示已注册网络设备列表的入口点,它指向列表的第一个元素(eth0)。然后各元素用next字段指向下一个元素(eth1)。使用ifconfig-a命令可以查看系统中所有已注册的网络设备。
net_device结构通过alloc_netdev函数分配,alloc_netdev函数位于net/core/dev.c文件中。该函数需要三个参数。
-
私有数据结构的大小
-
设备名,如eth0,eth1等。
-
配置例程,这些例程会初始化部分net_device字段。
分配成功则返回指向net_device结构的指针,分配失败则返回NULL。
在使用网络设备之前,必须对它进行初始化和向内核注册该设备。网络设备的初始化包括以下步骤:
-
硬件初始化:分配IRQ和I/O端口等。
-
软件初始化:分配IP地址等。
-
功能初始化:QoS等
网络设备(网卡)通过轮询和中断两种方式与内核沟通。
-
轮询(polling),由内核发起,内核周期性地检查网络设备是否有数据要处理。
-
中断(interrupt),由设备发起,设备向内核发送一个硬件中断信号。
Linux网络系统可以结合轮询和中断两方式以提高网络系统的性能。共小节重点介绍中断方式。
每个中断都会调用一个叫中断处理器的函数。当驱动程序向内核注册一个网卡时,会请求和分配一个IRQ号。接着为分配的这个IRQ注册中断处理器。注册和释放中断处理器的代码是架构相关的,不同的硬件平台有不同的代码实现。实现代码位于kernel/irq/manage.c和arch/XXX/kernel/irq.c源码文件中。XXX是不同硬件架构的名称,如我们所使用得最多的i386架构。下面是注册和释放中断处理器的函数原型。
int request_irq(unsigned int irq, irq_handler_t handler,unsigned long irqflags, const char *devname, void *dev_id)
void free_irq(unsigned int irq, void *dev_id)
内核是通过IRQ号来找到对应的中断处理器并执行它的。为了找到中断处理器,内核把IRQ号和中断处理器函数的关联起来存储在全局表(global table)中。IRQ号和中断处理器的关联性可以是一对一,也可以是一对多。因为IRQ号是可以被多个设备所共享的。
通过中断,网卡设备可以向驱动程序传送以下信息:
-
帧的接收,这是最用的中断类型。
-
传送失败通知,如传送超时。
-
DMA传送成功。
-
设备有足够的内存传送数据帧。当外出队列没有足够的内存空间存放一个最大的帧时(对于以太网卡是1535),网卡产生一个中断要求以后再传送数据,驱动程序会禁止数据的传送,。当有效内存空间多于设备需传送的最大帧(MTU)时,网卡会发送一个中断通知驱动程序重新启用数据传送。这些逻辑处理在网卡驱动程序中设计。netif_stop_queue()函数禁止设备传送队列,netif_start_queue()函数重启设备的传送队列。这些动作一般在驱动程序的xxx_start_xmit()中处理。
系统的中断资源是有限的,不可能为每种设备提供独立的中断号,多种设备要共享有限的中断号。上面我们提到中断号是和中断处理器关联的。在中断号共享的情况下内核如何正确找到对应的中断处理器呢?内核采用一种最简单的方法,就是不管三七二一,当同一中断号的中断发生时,与该中断号关联的所有中断处理器都一起被调用。调用后再靠中断处理器中的过滤程序来筛选执行真正的中断处理。
对于使用共享中断号的设备,它的驱动程序在注册时必须先指明允许中断共享。
IRQ与中断处理器的映射关系保存在一个矢量表中。该表保存了每个IRQ的中断处理器。矢量表的大小是平台相关的,从15(i386)到超过200都有。irqaction数据结构保存了映射表的信息。上面提到的request_irq()函数创建irqaction数据结构并通过setup_irq()把它加入到irq_des矢量表中。irq_des在kernel/irq/handler.c中定义,平台相关的定义在arch/XXX/kernel/irq.c文件中。setup_irq()在kernel/irq/manage.c,平台相关的定义在arch/XXX/kernel/irq.c中。
在系统启动阶段,网络设备操作层通过net_dev_init()进行初始化。net_dev_init()的代码在net/core/dev.c文件中。这是一个以__init标识的函数,表示它是一个低层的代码。
net_dev_init()的主要初始化工作内容包括以下几点:
-
生成/proc/net目录和目录下相关的文件。
kmod是内核模块加载器。该加载器在系统启动时会触发/sbin/modprobe和/sbin/hotplug自动加载相应的内核模块和运行设备启动脚本。modprobe使用/etc/modprobe.conf配置文件。当该文件中有"alias eth03c59x"配置时就会自动加3c59x.ko模块。
虚拟设备是在真实设备上的虚拟,虚拟设备和真实设备的对应关系可以一对多或多对一。即一个虚拟设备对应多个真实设备或多个真实设备一个虚拟设备。下面介绍网络子系统中虚拟设备的应用情况。
-
Bonding,把多个真实网卡虚拟成一个虚拟网卡。对于应用来讲就相当于访问一个网络接口。
-
802.1Q,802.3以太网帧头扩展,添加了VLAN头信息。把多个真实网卡虚拟成一个虚拟网卡。
-
Bridging,一个虚拟网桥,把多个真实网卡虚拟成一个虚拟网卡。
-
Tunnel interfaces,实现GRE和IP-over-IP虚拟通道。把一个真实网卡虚拟成多个虚拟网卡。
-
True equalizer (TEQL),类似于Bonding。
上面不是一个完整列表,随着内核的不断开发完善,新功能新应用也会不断出现。
程序调用流程:
module_init(rtl8139_init_module)static int __init rtl8139_init_module (void)
pci_register_driver(&rtl8139_pci_driver) #注册驱动程序
static int __devinit rtl8139_init_one (struct pci_dev *pdev,
const struct pci_device_id *ent)
static int __devinit rtl8139_init_board (struct pci_dev *pdev,
struct net_device **dev_out)
dev = alloc_etherdev (sizeof (*tp)) #为设备分配net_device数据结构
pci_enable_device (pdev) #激活PCI设备
pci_resource_start (pdev, 0) #获取PCI I/O区域1的首地址
pci_resource_end (pdev, 0) #获取PCI I/O区域1的尾地址
pci_resource_flags (pdev, 0) #获取PCI I/O区域1资源标记
pci_resource_len (pdev, 0) #获取区域资源长度
pci_resource_start (pdev, 1) #获取PCI I/O区域2的首地址
pci_resource_end (pdev, 1) #获取PCI I/O区域2的尾地址
pci_resource_flags (pdev, 1) #获取PCI I/O区域2资源标记
pci_resource_len (pdev, 1) #获取区域资源长度
pci_request_regions(pdev, DRV_NAME) #检查其它PCI设备是否使用了相同的地址资源
pci_set_master(pdev) #通过设置PCI设备的命令寄存器允许DMA
网络报文从应用程序产生,通过网卡发送,在另一端的网卡接收数据并传递给应用程序。这个过程网络报文在内核中调用了一系列的函数。下面把这些函数列举出来,方便我们了解网络报文的流程。
发送流程:
write|
sys_write
|
sock_sendmsg
|
inet_sendmsg
|
tcp_sendmsg
|
tcp_push_one
|
tcp_transmit_skb
|
ip_queue_xmit
|
ip_route_output
|
ip_queue_xmit
|
ip_queue_xmit2
|
ip_output
|
ip_finish_output
|
neith_connected_output
|
dev_queue_xmit ----------------|
| |
| queue_run
| queue_restart
| |
hard_start_xmit-----------------
接收流程:
netif_rx|
netif_rx_schedule
|
_cpu_raise_softirq
|
net_rx_action
|
ip_rcv
|
ip_rcv_finish
|
ip_route_input
|
ip_local_deliver
|
ip_local_deliver_finish
|
tcp_v4_rcv
|
tcp_v4_do_rcv
|
tcp_rcv_established------------------|
| |
tcp_data_queue |
| |
_skb_queue_tail----------------------|
|
data_ready
|
sock_def_readable
|
wake_up_interruptible
|
tcp_data_wait
|
tcp_recvmsg
|
inet_recvmsg
|
sock_recvmsg
|
sock_read
|
read
数据包在应用层称为data,在TCP层称为segment,在IP层称为packet,在数据链路层称为frame。 |
-
在编译使用pcap包的程序时出现undefined reference to `pcap_open_live'出错提示。
出错原因是gcc找不到pcap的静态链接库文件,在编译时加-lpcap参数就可以了。
-
字符设备和块设备都有主设备号和次设备号,主设备号用来标记设备的驱动程序,次设备号用来区分同一驱动程序下的不同的设备。
-
在编程思路上,机制表示需要提供什么功能,策略表示如何使用这些功能。区分机制和策略是UNIX设计最重要和最好的思想之一。如X系统就由X服务器和X客户端组成。X服务器实现机制,负责操作硬件,给用户程序提供一个统一的接口。而X客户端实现策略,负责如何使用X服务器提供的功能。
-
内核目录清单
Documentation/ 关于内核的各种文档
arch/ 与平台有关的代码
crypto/(2.6) 加密代码
drivers/ 设备驱动程序
fs/ 文件系统
include/ 内核代码的头文件
init/ 内核的初始化代码
ipc/ System V进程间通信
kernel/ 进程,timing,程序运行,信号,模块等核心代码
lib/ 内核内部使用的库函数
mm/ 内存管理
net/ 网络协议栈
scripts/ 编译内核时用到的shell脚本和程序
security/(2.6) 安全
usr/(2.6) initramfs的实例