徽娘苑心

徽娘苑心

 

碧池徽墨染,徽娘夜苍茫
一腔殷殷血,染红嫁衣裳
路尽头,孤身只影伴残阳
柔纫丝千缕,却是无情网
头顶石牌坊,脚下青石板
这日子,总是石头那么凉
幽幽黛瓦,高高粉墙
故人已去,余音绕梁
春波才暖,秋水更凉
眼前空留,半抹池塘
悠悠琴声,痴痴徽娘
欲说还休,语焉不详
夏夜恨短,冬日叹长
胸中空有,一腔衷肠

Solaris 程序开发相关书籍

Solaris 10 Software Developer Collection – Simplified Chinese

原文地址:http://docs.sun.com/app/docs/coll/1583.1?l=zh&q=819-6959

Linux 调试技术 MEMWATCH  YAMD

用户空间工具

        

  • 内存工具:MEMWATCH 和 YAMD
  •     

  • strace
  •     

  • GNU 调试器(gdb)
  •     

  • 魔术键控顺序

C 语言作为 Linux 系统上标准的编程语言给予了我们对动态内存分配很大的控制权。然而,这种自由可能会导致严重的内存管理问题,而这些问题可能导致程序崩溃或随时间的推移导致性能降级。

内存泄漏(即 malloc() 内存在对应的 free() 调用执行后永不被释放)和缓冲区溢出(例如对以前分配到某数组的内存进行写操作)是一些常见的问题,它们可能很难检测到。这一部分将讨论几个调试工具,它们极大地简化了检测和找出内存问题的过程。

MEMWATCH 由 Johan Lindh 编写,是一个开放源代码 C 语言内存错误检测工具,您可以自己下载它(请参阅本文后面部分的参考资料)。只要在代码中添加一个头文件并在 gcc 语句中定义了 MEMWATCH 之后,您就可以跟踪程序中的内存泄漏和错误了。MEMWATCH 支持 ANSI C,它提供结果日志纪录,能检测双重释放(double-free)、错误释放(erroneous free)、没有释放的内存(unfreed memory)、溢出和下溢等等。

原文地址:http://www-128.ibm.com/developerworks/cn/linux/sdk/l-debug/index.html

嵌入式linux下gdbserver调试共享库

嵌入式linux下gdbserver调试共享库

开发嵌入式系统时,调试往往是一大难题。面试过不少嵌入式linux工程师,当问及调试手段时,他们的调试手段一般是两种:首先是在PC上的模拟环境中运行,若有问题,可以很方便的调试。其次,若在板子上运行时才出错,就用printf输出log信息,根据log信息定位错误。有少部分人用gdbserver调试板子上的程序,但问到如何在共享库里设置断点时,都说没有办法。

去年,Tinyx的一个内存越界BUG,花了我2天时间。gcc的一个浮点数BUG让我查了3天时间。这类BUG在PC上根本重现不了,在板子上用printf要花费大量的时间才能把错误的范围缩小一点。后来想了想,与其花时间去加printf,还不如把gdbserver调试共享库的问题解决了,可以为以后的调试节省不少时间。
在网上找了半天资料,没有什么收获,看来只好自己动手研究。花了一个周末的时间去研究gdbserver的运行方式。办法是找到了,不过仍然有点麻烦,等有时间了,修改一下gdb的代码,把这个过程自动化了。
先调试运行gdbserver,对gdbserver有了一些感性认识,然后研究linux-low.c中的代码。原来,设置断点只是在对应的内存中写入断点指令(x86上为0xcc)。
gdbserver为什么不能在共享库中设置断点呢?设置断点只是写内存,调试时,所有的代码段都是可写的,在exe中可以设置断点,没有理由不让在共享库中设置啊。所以这应该与是否是共享库关系不大。
猜测可能是符号与地址对应关系有误,如果你的本意为function1设置断点,结果gdb搞错了,设置一个毫不相干的地方,可能永远都不会执行到那里,这个断点自然没什么效果
如果是这样,有两种方法可以解决:要么手动计算符号的地址,再设置断点,当然这样太累。另外就让gdb自动对应起来。经过反得尝试,用下列方法可以在共享库中设置断点,虽然有点麻烦,还是可行的。
1. 准备工作,编写下面几个文件:
test.c:
#include <stdlib.h>
int test(int a, int b)
{
int s = a + b;
printf("%d\n", s);
return s;
}
main.c:
#include <stdlib.h>
extern int test(int a, int b);
int main(int argc, char* argv[])
{
int s = test(10, 20);
return s;
}
Makefile:
all: so main
so:
gcc -g test.c -shared -o libtest.so
main:
gcc -g main.c -L./ -ltest -o test.exe
clean:
rm -f *.exe *.so
(为了便于演示,整个过程在PC上测试,后来证实在实验板上能够正常工作)
2. 编译并设置环境变量
make
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
3. 运行gdbserver
gdbserver localhost:2000 ./test.exe
4. 运行gdb客户端
gdb
symbol-file test.exe
target remote localhost:2000
b main
c
5. 查看libtest.so的代码在内存中的位置。
(从gdbserver的输出或者用ps可以得到test.exe的进程ID,这里假设PID是11547)
cat /proc/11547/maps
输出:
00624000-0063e000 r-xp 00000000 03:01 718192 /lib/ld-2.3.5.so
0063e000-0063f000 r-xp 00019000 03:01 718192 /lib/ld-2.3.5.so
0063f000-00640000 rwxp 0001a000 03:01 718192 /lib/ld-2.3.5.so
00642000-00766000 r-xp 00000000 03:01 718193 /lib/libc-2.3.5.so
00766000-00768000 r-xp 00124000 03:01 718193 /lib/libc-2.3.5.so
00768000-0076a000 rwxp 00126000 03:01 718193 /lib/libc-2.3.5.so
0076a000-0076c000 rwxp 0076a000 00:00 0
00bbe000-00bbf000 r-xp 00bbe000 00:00 0
00fcc000-00fcd000 r-xp 00000000 03:01 1238761 /root/test/gdbservertest/libtest.so
00fcd000-00fce000 rwxp 00000000 03:01 1238761 /root/test/gdbservertest/libtest.so
08048000-08049000 r-xp 00000000 03:01 1238765 /root/test/gdbservertest/test.exe
08049000-0804a000 rw-p 00000000 03:01 1238765 /root/test/gdbservertest/test.exe
b7f8a000-b7f8b000 rw-p b7f8a000 00:00 0
b7f99000-b7f9a000 rw-p b7f99000 00:00 0
bfd85000-bfd9a000 rw-p bfd85000 00:00 0 [stack]
由此可以知道:libtest.so的代码在00fcc000-00fcd000之间。
6. 查看libtest.so的.text段在内存中的偏移位置:
objdump -h libtest.so |grep .text
输出:
9 .text 00000130 00000450 00000450 00000450 2**4
即偏移位置为0x00000450
7. 回到gdb窗口,加载libtest.so的符号表。
add-symbol-file libtest.so 0x00fcc450
(这里0x00fcc450 = 0x00fcc000 + 0x00000450)
8. 在共享库的函数中设置断点。
b test
9. 继续调试,可以发现在共享库中设置的断点,能够正常工作。

linux tc 手册

linux tc

名字
  tc — 显示/维护流量控制设置
摘要
tc qdisc [ add | change | replace | link ] dev DEV [ parent qdisc-id | root ] [ handle qdisc-id ] qdisc [ qdisc specific parameters ]

tc class [ add | change | replace ] dev DEV parent qdisc-id [ classid class-id ] qdisc [ qdisc specific parameters ]

tc filter [ add | change | replace ] dev DEV [ parent qdisc-id | root ] protocol protocol prio priority filtertype [ filtertype specific parameters ] flowid flow-id

tc [-s | -d ] qdisc show [ dev DEV ]

tc [-s | -d ] class show dev DEV tc filter show dev DEV

简介
Tc用于Linux内核的流量控制。流量控制包括以下几种方式:

SHAPING(限制)
当流量被限制,它的传输速率就被控制在某个值以下。限制值可以大大小于有效带宽,这样可以平滑突发数据流量,使网络更为稳定。shaping(限制)只适用于向外的流量。

SCHEDULING(调度)
通过调度数据包的传输,可以在带宽范围内,按照优先级分配带宽。SCHEDULING(调度)也只适于向外的流量。

POLICING(策略)
SHAPING用于处理向外的流量,而POLICIING(策略)用于处理接收到的数据。

DROPPING(丢弃)
如果流量超过某个设定的带宽,就丢弃数据包,不管是向内还是向外。

流量的处理由三种对象控制,它们是:qdisc(排队规则)、class(类别)和filter(过滤器)。

QDISC(排队规则)
QDisc(排队规则)是queueing discipline的简写,它是理解流量控制(traffic control)的基础。无论何时,内核如果需要通过某个网络接口发送数据包,它都需要按照为这个接口配置的qdisc(排队规则)把数据包加入队列。然后,内核会尽可能多地从qdisc里面取出数据包,把它们交给网络适配器驱动模块
最简单的QDisc是pfifo它不对进入的数据包做任何的处理,数据包采用先入先出的方式通过队列。不过,它会保存网络接口一时无法处理的数据包。

CLASS(类)
某些QDisc(排队规则)可以包含一些类别,不同的类别中可以包含更深入的QDisc(排队规则),通过这些细分的QDisc还可以为进入的队列的数据包排队。通过设置各种类别数据包的离队次序,QDisc可以为设置网络数据流量的优先级。

FILTER(过滤器)
filter(过滤器)用于为数据包分类,决定它们按照何种QDisc进入队列。无论何时数据包进入一个划分子类的类别中,都需要进行分类。分类的方法可以有多种,使用fileter(过滤器)就是其中之一。使用filter(过滤器)分类时,内核会调用附属于这个类(class)的所有过滤器,直到返回一个判决。如果没有判决返回,就作进一步的处理,而处理方式和QDISC有关。
需要注意的是,filter(过滤器)是在QDisc内部,它们不能作为主体。

CLASSLESS QDisc(不可分类QDisc)
无类别QDISC包括:
[p|b]fifo
使用最简单的qdisc,纯粹的先进先出。只有一个参数:limit,用来设置队列的长度,pfifo是以数据包的个数为单位;bfifo是以字节数为单位。
pfifo_fast
在编译内核时,如果打开了高级路由器(Advanced Router)编译选项,pfifo_fast就是系统的标准QDISC。它的队列包括三个波段(band)。在每个波段里面,使用先进先出规则。而三个波段(band)的优先级也不相同,band 0的优先级最高,band 2的最低。如果band里面有数据包,系统就不会处理band 1里面的数据包,band 1和band 2之间也是一样。数据包是按照服务类型(Type of Service,TOS)被分配多三个波段(band)里面的。
red
red是Random Early Detection(随机早期探测)的简写。如果使用这种QDISC,当带宽的占用接近于规定的带宽时,系统会随机地丢弃一些数据包。它非常适合高带宽应用。
sfq
sfq是Stochastic Fairness Queueing的简写。它按照会话(session–对应于每个TCP连接或者UDP流)为流量进行排序,然后循环发送每个会话的数据包。
tbf
tbf是Token Bucket Filter的简写,适合于把流速降低到某个值。
不可分类QDisc的配置
如果没有可分类QDisc,不可分类QDisc只能附属于设备的根。它们的用法如下:
tc qdisc add dev DEV root QDISC QDISC-PARAMETERS

要删除一个不可分类QDisc,需要使用如下命令:

tc qdisc del dev DEV root

一个网络接口上如果没有设置QDisc,pfifo_fast就作为缺省的QDisc。

CLASSFUL QDISC(分类QDisc)
可分类的QDisc包括:
CBQ
CBQ是Class Based Queueing(基于类别排队)的缩写。它实现了一个丰富的连接共享类别结构,既有限制(shaping)带宽的能力,也具有带宽优先级管理的能力。带宽限制是通过计算连接的空闲时间完成的。空闲时间的计算标准是数据包离队事件的频率和下层连接(数据链路层)的带宽。
HTB
HTB是Hierarchy Token Bucket的缩写。通过在实践基础上的改进,它实现了一个丰富的连接共享类别体系。使用HTB可以很容易地保证每个类别的带宽,虽然它也允许特定的类可以突破带宽上限,占用别的类的带宽。HTB可以通过TBF(Token Bucket Filter)实现带宽限制,也能够划分类别的优先级。
PRIO
PRIO QDisc不能限制带宽,因为属于不同类别的数据包是顺序离队的。使用PRIO QDisc可以很容易对流量进行优先级管理,只有属于高优先级类别的数据包全部发送完毕,才会发送属于低优先级类别的数据包。为了方便管理,需要使用iptables或者ipchains处理数据包的服务类型(Type Of Service,ToS)。
操作原理
类(Class)组成一个树,每个类都只有一个父类,而一个类可以有多个子类。某些QDisc(例如:CBQ和HTB)允许在运行时动态添加类,而其它的QDisc(例如:PRIO)不允许动态建立类。
允许动态添加类的QDisc可以有零个或者多个子类,由它们为数据包排队。

此外,每个类都有一个叶子QDisc,默认情况下,这个叶子QDisc使用pfifo的方式排队,我们也可以使用其它类型的QDisc代替这个默认的QDisc。而且,这个叶子叶子QDisc有可以分类,不过每个子类只能有一个叶子QDisc。

当一个数据包进入一个分类QDisc,它会被归入某个子类。我们可以使用以下三种方式为数据包归类,不过不是所有的QDisc都能够使用这三种方式。

tc过滤器(tc filter)
如果过滤器附属于一个类,相关的指令就会对它们进行查询。过滤器能够匹配数据包头所有的域,也可以匹配由ipchains或者iptables做的标记。
服务类型(Type of Service)
某些QDisc有基于服务类型(Type of Service,ToS)的内置的规则为数据包分类。
skb->priority
用户空间的应用程序可以使用SO_PRIORITY选项在skb->priority域设置一个类的ID。
树的每个节点都可以有自己的过滤器,但是高层的过滤器也可以直接用于其子类。
如果数据包没有被成功归类,就会被排到这个类的叶子QDisc的队中。相关细节在各个QDisc的手册页中。

命名规则
所有的QDisc、类和过滤器都有ID。ID可以手工设置,也可以有内核自动分配。
ID由一个主序列号和一个从序列号组成,两个数字用一个冒号分开。

QDISC
一个QDisc会被分配一个主序列号,叫做句柄(handle),然后把从序列号作为类的命名空间。句柄采用象10:一样的表达方式。习惯上,需要为有子类的QDisc显式地分配一个句柄。

类(CLASS)
在同一个QDisc里面的类分享这个QDisc的主序列号,但是每个类都有自己的从序列号,叫做类识别符(classid)。类识别符只与父QDisc有关,和父类无关。类的命名习惯和QDisc的相同。

过滤器(FILTER)
过滤器的ID有三部分,只有在对过滤器进行散列组织才会用到。详情请参考tc-filters手册页。
单位
tc命令的所有参数都可以使用浮点数,可能会涉及到以下计数单位。
带宽或者流速单位:

kbps
千字节/秒
mbps
兆字节/秒
kbit
KBits/秒
mbit
MBits/秒
bps或者一个无单位数字
字节数/秒
数据的数量单位:

kb或者k
千字节
mb或者m
兆字节
mbit
兆bit
kbit
千bit
b或者一个无单位数字
字节数
时间的计量单位:
s、sec或者secs

ms、msec或者msecs
分钟
us、usec、usecs或者一个无单位数字
微秒

TC命令
tc可以使用以下命令对QDisc、类和过滤器进行操作:
add
在一个节点里加入一个QDisc、类或者过滤器。添加时,需要传递一个祖先作为参数,传递参数时既可以使用ID也可以直接传递设备的根。如果要建立一个QDisc或者过滤器,可以使用句柄(handle)来命名;如果要建立一个类,可以使用类识别符(classid)来命名。

remove
删除有某个句柄(handle)指定的QDisc,根QDisc(root)也可以删除。被删除QDisc上的所有子类以及附属于各个类的过滤器都会被自动删除。

change
以替代的方式修改某些条目。除了句柄(handle)和祖先不能修改以外,change命令的语法和add命令相同。换句话说,change命令不能一定节点的位置。

replace
对一个现有节点进行近于原子操作的删除/添加。如果节点不存在,这个命令就会建立节点。

link
只适用于DQisc,替代一个现有的节点。

历史
tc由Alexey N. Kuznetsov编写,从Linux 2.2版开始并入Linux内核。
SEE ALSO
tc-cbq(8)、tc-htb(8)、tc-sfq(8)、tc-red(8)、tc-tbf(8)、tc-pfifo(8)、tc-bfifo(8)、tc-pfifo_fast(8)、tc-filters(8)
Linux从kernel 2.1.105开始支持QOS,不过,需要重新编译内核。运行make config时将EXPERIMENTAL _OPTIONS设置成y,并且将Class Based Queueing (CBQ), Token Bucket Flow, Traffic Shapers 设置为 y ,运行 make dep; make clean; make bzilo,生成新的内核。

  在Linux操作系统中流量控制器(TC)主要是在输出端口处建立一个队列进行流量控制,控制的方式是基于路由,亦即基于目的IP地址或目的子网的网络号的流量控制。流量控制器TC,其基本的功能模块为队列、分类和过滤器。Linux内核中支持的队列有,Class Based Queue ,Token Bucket Flow ,CSZ ,First In First Out ,Priority ,TEQL ,SFQ ,ATM ,RED。这里我们讨论的队列与分类都是基于CBQ(Class Based Queue)的,而过滤器是基于路由(Route)的。

  配置和使用流量控制器TC,主要分以下几个方面:分别为建立队列、建立分类、建立过滤器和建立路由,另外还需要对现有的队列、分类、过滤器和路由进行监视。

  其基本使用步骤为:

  1) 针对网络物理设备(如以太网卡eth0)绑定一个CBQ队列;

  2) 在该队列上建立分类;

  3) 为每一分类建立一个基于路由的过滤器;

  4) 最后与过滤器相配合,建立特定的路由表。

  先假设一个简单的环境

  流量控制器上的以太网卡(eth0) 的IP地址为192.168.1.66,在其上建立一个CBQ队列。假设包的平均大小为1000字节,包间隔发送单元的大小为8字节,可接收冲突的发送最长包数目为20字节。

  假如有三种类型的流量需要控制:

  1) 是发往主机1的,其IP地址为192.168.1.24。其流量带宽控制在8Mbit,优先级为2;

  2) 是发往主机2的,其IP地址为192.168.1.26。其流量带宽控制在1Mbit,优先级为1;

  3) 是发往子网1的,其子网号为192.168.1.0,子网掩码为255.255.255.0。流量带宽控制在1Mbit,优先级为6。

  1. 建立队列

  一般情况下,针对一个网卡只需建立一个队列。

  将一个cbq队列绑定到网络物理设备eth0上,其编号为1:0;网络物理设备eth0的实际带宽为10 Mbit,包的平均大小为1000字节;包间隔发送单元的大小为8字节,最小传输包大小为字节。

  ?tc qdisc add dev eth0 root handle 1: cbq bandwidth 10Mbit avpkt 1000 cell 8 mpu

  2. 建立分类

  分类建立在队列之上。一般情况下,针对一个队列需建立一个根分类,然后再在其上建立子分类。对于分类,按其分类的编号顺序起作用,编号小的优先;一旦符合某个分类匹配规则,通过该分类发送数据包,则其后的分类不再起作用。

  1) 创建根分类1:1;分配带宽为10Mbit,优先级别为8。

  ?tc class add dev eth0 parent 1:0 classid 1:1 cbq bandwidth 10Mbit rate 10Mbit maxburst 20 allot 1514 prio 8 avpkt 1000 cell 8 weight 1Mbit

  该队列的最大可用带宽为10Mbit,实际分配的带宽为10Mbit,可接收冲突的发送最长包数目为20字节;最大传输单元加MAC头的大小为1514字节,优先级别为8,包的平均大小为1000字节,包间隔发送单元的大小为8字节,相应于实际带宽的加权速率为1Mbit。

  2)创建分类1:2,其父分类为1:1,分配带宽为8Mbit,优先级别为2。

  ?tc class add dev eth0 parent 1:1 classid 1:2 cbq bandwidth 10Mbit rate 8Mbit maxburst 20 allot 1514 prio 2 avpkt 1000 cell 8 weight 800Kbit split 1:0 bounded

  该队列的最大可用带宽为10Mbit,实际分配的带宽为 8Mbit,可接收冲突的发送最长包数目为20字节;最大传输单元加MAC头的大小为1514字节,优先级别为1,包的平均大小为1000字节,包间隔发送单元的大小为8字节,相应于实际带宽的加权速率为800Kbit,分类的分离点为1:0,且不可借用未使用带宽。

  3)创建分类1:3,其父分类为1:1,分配带宽为1Mbit,优先级别为1。

  ?tc class add dev eth0 parent 1:1 classid 1:3 cbq bandwidth 10Mbit rate 1Mbit maxburst 20 allot 1514 prio 1 avpkt 1000 cell 8 weight 100Kbit split 1:0

  该队列的最大可用带宽为10Mbit,实际分配的带宽为 1Mbit,可接收冲突的发送最长包数目为20字节;最大传输单元加MAC头的大小为1514字节,优先级别为2,包的平均大小为1000字节,包间隔发送单元的大小为8字节,相应于实际带宽的加权速率为100Kbit,分类的分离点为1:0。

  4)创建分类1:4,其父分类为1:1,分配带宽为1Mbit,优先级别为6。

  ?tc class add dev eth0 parent 1:1 classid 1:4 cbq bandwidth 10Mbit rate 1Mbit maxburst 20 allot 1514 prio 6 avpkt 1000 cell 8 weight 100Kbit split 1:0

  该队列的最大可用带宽为10Mbit,实际分配的带宽为 Kbit,可接收冲突的发送最长包数目为20字节;最大传输单元加MAC头的大小为1514字节,优先级别为1,包的平均大小为1000字节,包间隔发送单元的大小为8字节,相应于实际带宽的加权速率为100Kbit,分类的分离点为1:0。

  3. 建立过滤器

  过滤器主要服务于分类。一般只需针对根分类提供一个过滤器,然后为每个子分类提供路由映射。

  1) 应用路由分类器到cbq队列的根,父分类编号为1:0;过滤协议为ip,优先级别为100,过滤器为基于路由表。

  ?tc filter add dev eth0 parent 1:0 protocol ip prio 100 route

  2) 建立路由映射分类1:2, 1:3, 1:4

  ?tc filter add dev eth0 parent 1:0 protocol ip prio 100 route to 2 flowid 1:2

  ?tc filter add dev eth0 parent 1:0 protocol ip prio 100 route to 3 flowid 1:3

  ?tc filter add dev eth0 parent 1:0 protocol ip prio 100 route to 4 flowid 1:4

  4.建立路由

  该路由是与前面所建立的路由映射一一对应。

  1) 发往主机192.168.1.24的数据包通过分类2转发(分类2的速率8Mbit)

  ?ip route add 192.168.1.24 dev eth0 via 192.168.1.66 realm 2

  2) 发往主机192.168.1.30的数据包通过分类3转发(分类3的速率1Mbit)

  ?ip route add 192.168.1.30 dev eth0 via 192.168.1.66 realm 3

  3)发往子网192.168.1.0/24的数据包通过分类4转发(分类4的速率1Mbit)

  ?ip route add 192.168.1.0/24 dev eth0 via 192.168.1.66 realm 4

  注:一般对于流量控制器所直接连接的网段建议使用IP主机地址流量控制限制,不要使用子网流量控制限制。如一定需要对直连子网使用子网流量控制限制,则在建立该子网的路由映射前,需将原先由系统建立的路由删除,才可完成相应步骤。

  5. 监视

  主要包括对现有队列、分类、过滤器和路由的状况进行监视。

  1)显示队列的状况

  简单显示指定设备(这里为eth0)的队列状况

?tc qdisc ls dev eth0
qdisc cbq 1: rate 10Mbit (bounded,isolated) prio no-transmit

 

  详细显示指定设备(这里为eth0)的队列状况

?tc -s qdisc ls dev eth0
qdisc cbq 1: rate 10Mbit (bounded,isolated) prio no-transmit
Sent 76731 bytes 13232 pkts (dropped 0, overlimits 0)
borrowed 0 overactions 0 avgidle 31 undertime 0

 

  这里主要显示了通过该队列发送了13232个数据包,数据流量为76731个字节,丢弃的包数目为0,超过速率限制的包数目为0。

  2)显示分类的状况

  简单显示指定设备(这里为eth0)的分类状况

?tc class ls dev eth0
class cbq 1: root rate 10Mbit (bounded,isolated) prio no-transmit
class cbq 1:1 parent 1: rate 10Mbit prio no-transmit #no-transmit表示优先级为8
class cbq 1:2 parent 1:1 rate 8Mbit prio 2
class cbq 1:3 parent 1:1 rate 1Mbit prio 1
class cbq 1:4 parent 1:1 rate 1Mbit prio 6

 

  详细显示指定设备(这里为eth0)的分类状况

?tc -s class ls dev eth0
class cbq 1: root rate 10Mbit (bounded,isolated) prio no-transmit
Sent 17725304 bytes 32088 pkts (dropped 0, overlimits 0)
borrowed 0 overactions 0 avgidle 31 undertime 0
class cbq 1:1 parent 1: rate 10Mbit prio no-transmit
Sent 16627774 bytes 28884 pkts (dropped 0, overlimits 0)
borrowed 16163 overactions 0 avgidle 587 undertime 0
class cbq 1:2 parent 1:1 rate 8Mbit prio 2
Sent 628829 bytes 3130 pkts (dropped 0, overlimits 0)
borrowed 0 overactions 0 avgidle 4137 undertime 0
class cbq 1:3 parent 1:1 rate 1Mbit prio 1
Sent 0 bytes 0 pkts (dropped 0, overlimits 0)
borrowed 0 overactions 0 avgidle 159654 undertime 0
class cbq 1:4 parent 1:1 rate 1Mbit prio 6
Sent 5552879 bytes 8076 pkts (dropped 0, overlimits 0)
borrowed 3797 overactions 0 avgidle 159557 undertime 0

 

  这里主要显示了通过不同分类发送的数据包,数据流量,丢弃的包数目,超过速率限制的包数目等等。其中根分类(class cbq 1:0)的状况应与队列的状况类似。

  例如,分类class cbq 1:4发送了8076个数据包,数据流量为5552879个字节,丢弃的包数目为0,超过速率限制的包数目为0。

  显示过滤器的状况

?tc -s filter ls dev eth0
filter parent 1: protocol ip pref 100 route
filter parent 1: protocol ip pref 100 route fh 0xffff0002 flowid 1:2 to 2
filter parent 1: protocol ip pref 100 route fh 0xffff0003 flowid 1:3 to 3
filter parent 1: protocol ip pref 100 route fh 0xffff0004 flowid 1:4 to 4

 

  这里flowid 1:2代表分类class cbq 1:2,to 2代表通过路由2发送。

  显示现有路由的状况

?ip route
192.168.1.66 dev eth0 scope link
192.168.1.24 via 192.168.1.66 dev eth0 realm 2
202.102.24.216 dev ppp0 proto kernel scope link src 202.102.76.5
192.168.1.30 via 192.168.1.66 dev eth0 realm 3
192.168.1.0/24 via 192.168.1.66 dev eth0 realm 4
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.66
172.16.1.0/24 via 192.168.1.66 dev eth0 scope link
127.0.0.0/8 dev lo scope link
default via 202.102.24.216 dev ppp0
default via 192.168.1.254 dev eth0

 

  如上所示,结尾包含有realm的显示行是起作用的路由过滤器。

  6. 维护

  主要包括对队列、分类、过滤器和路由的增添、修改和删除。

    增添动作一般依照"队列->分类->过滤器->路由"的顺序进行;修改动作则没有什么要求;删除则依照"路由->过滤器->分类->队列"的顺序进行。

  1)队列的维护

  一般对于一台流量控制器来说,出厂时针对每个以太网卡均已配置好一个队列了,通常情况下对队列无需进行增添、修改和删除动作了。

  2)分类的维护

  增添

  增添动作通过tc class add命令实现,如前面所示。

  修改

  修改动作通过tc class change命令实现,如下所示:

?tc class change dev eth0 parent 1:1 classid 1:2 cbq bandwidth 10Mbit
rate 7Mbit maxburst 20 allot 1514 prio 2 avpkt 1000 cell
8 weight 700Kbit split 1:0 bounded

 

  对于bounded命令应慎用,一旦添加后就进行修改,只可通过删除后再添加来实现。

  删除

  删除动作只在该分类没有工作前才可进行,一旦通过该分类发送过数据,则无法删除它了。因此,需要通过shell文件方式来修改,通过重新启动来完成删除动作。

  3)过滤器的维护

  增添

  增添动作通过tc filter add命令实现,如前面所示。

  修改

  修改动作通过tc filter change命令实现,如下所示:

?tc filter change dev eth0 parent 1:0 protocol ip prio 100 route to
10 flowid 1:8

 

  删除

  删除动作通过tc filter del命令实现,如下所示:

?tc filter del dev eth0 parent 1:0 protocol ip prio 100 route to 10

 

  4)与过滤器一一映射路由的维护

  增添

  增添动作通过ip route add命令实现,如前面所示。

  修改

  修改动作通过ip route change命令实现,如下所示:

?ip route change 192.168.1.30 dev eth0 via 192.168.1.66 realm 8

 

  删除

  删除动作通过ip route del命令实现,如下所示:

?ip route del 192.168.1.30 dev eth0 via 192.168.1.66 realm 8
?ip route del 192.168.1.0/24 dev eth0 via 192.168.1.66 realm 4

Linux系统调用讲义

Linux系统调用讲义

Linux下系统调用的实现
Linux中的系统调用
Linux中怎样编译和定制内核
 

        

  • Linux下系统调用的实现
        

  1. Unix/Linux操作系统的体系结构及系统调用介绍
        

              

    1. 什么是操作系统和系统调用         

                  操作系统是从硬件抽象出来的虚拟机,在该虚拟机上用户可以运行应用程序。它负责直接与硬件交互,向用户程序提供公共服务,并使它们同硬件特性隔离。因为程序不应该依赖于下层的硬件,只有这样应用程序才能很方便的在各种不同的Unix系统之间移动。系统调用是Unix/Linux操作系统向用户程序提供支持的接口,通过这些接口应用程序向操作系统请求服务,控制转向操作系统,而操作系统在完成服务后,将控制和结果返回给用户程序。
               

              

    2.         

    3. Unix/Linux系统体系结构        

              一个Unix/Linux系统分为三个层次:用户、核心以及硬件。
              
                   其中系统调用是用户程序与核心间的边界,通过系统调用进程可由用户模式转入核心模式,在核心模式下完成一定的服务请求后在返回用户模式。

              

          系统调用接口看起来和C程序中的普通函数调用很相似,它们通常是通过库把这些函数调用映射成进入操作系统所需要的原语。

              

          这些操作原语只是提供一个基本功能集,而通过库对这些操作的引用和封装,可以形成丰富而且强大的系统调用库。这里体现了机制与策略相分离的编程思想——系统调用只是提供访问核心的基本机制,而策略是通过系统调用库来体现。

              

      例:execv, execl, execlv, opendir , readdir…

              

       

              

    4.         

    5. Unix/Linux运行模式,地址空间和上下文
                       

       

              

      运行模式(运行态):

              

          一种计算机硬件要运行Unix/Linux系统,至少需要提供两种运行模式:高优先级的核心模式和低优先级的用户模式。

              

          实际上许多计算机都有两种以上的执行模式。如:intel 80×86体系结构就有四层执行特权,内层特权最高。Unix只需要两层即可以了:核心运行在高优先级,称之为核心态;其它外围软件包括shell,编辑程序,Xwindow等等都是在低优先级运行,称之为用户态。之所以采取不同的执行模式主要原因时为了保护,由于用户进程在较低的特权级上运行,它们将不能意外或故意的破坏其它进程或内核。程序造成的破坏会被局部化而不影响系统中其它活动或者进程。当用户进程需要完成特权模式下才能完成的某些功能时,必须严格按照系统调用提供接口才能进入特权模式,然后执行调用所提供的有限功能。

              

          每种运行态都应该有自己的堆栈。在Linux中,分为用户栈和核心栈。用户栈包括在用户态执行时函数调用的参数、局部变量和其它数据结构。有些系统中专门为全局中断处理提供了中断栈,但是x86中并没有中断栈,中断在当前进程的核心栈中处理。

              

      地址空间:

              

          采用特权模式进行保护的根本目的是对地址空间的保护,用户进程不应该能够访问所有的地址空间:只有通过系统调用这种受严格限制的接口,进程才能进入核心态并访问到受保护的那一部分地址空间的数据,这一部分通常是留给操作系统使用。另外,进程与进程之间的地址空间也不应该随便互访。这样,就需要提供一种机制来在一片物理内存上实现同一进程不同地址空间上的保护,以及不同进程之间地址空间的保护。

              

          Unix/Linux中通过虚存管理机制很好的实现了这种保护,在虚存系统中,进程所使用的地址不直接对应物理的存储单元。每个进程都有自己的虚存空间,每个进程有自己的虚拟地址空间,对虚拟地址的引用通过地址转换机制转换成为物理地址的引用。正因为所有进程共享物理内存资源,所以必须通过一定的方法来保护这种共享资源,通过虚存系统很好的实现了这种保护:每个进程的地址空间通过地址转换机制映射到不同的物理存储页面上,这样就保证了进程只能访问自己的地址空间所对应的页面而不能访问或修改其它进程的地址空间对应的页面。

              

          虚拟地址空间分为两个部分:用户空间和系统空间。在用户模式下只能访问用户空间而在核心模式下可以访问系统空间和用户空间。系统空间在每个进程的虚拟地址空间中都是固定的,而且由于系统中只有一个内核实例在运行,因此所有进程都映射到单一内核地址空间。内核中维护全局数据结构和每个进程的一些对象信息,后者包括的信息使得内核可以访问任何进程的地址空间。通过地址转换机制进程可以直接访问当前进程的地址空间(通过MMU),而通过一些特殊的方法也可以访问到其它进程的地址空间。

              

          尽管所有进程都共享内核,但是系统空间是受保护的,进程在用户态无法访问。进程如果需要访问内核,则必须通过系统调用接口。进程调用一个系统调用时,通过执行一组特殊的指令(这个指令是与平台相关的,每种系统都提供了专门的trap命令,基于x86Linux中是使用int 指令)使系统进入内核态,并将控制权交给内核,由内核替代进程完成操作。当系统调用完成后,内核执行另一组特征指令将系统返回到用户态,控制权返回给进程。

              

      上下文:

              

          一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。

              

          用户级上下文:正文、数据、用户栈以及共享存储区;

              

          寄存器上下文:程序寄存器(IP),即CPU将执行的下条指令地址,处理机状态寄存器(EFLAGS),栈指针,通用寄存器;

              

          系统级上下文:进程表项(proc结构)U区,在Linux中这两个部分被合成task_struct,区表及页表(mm_struct , vm_area_struct, pgd, pmd, pte),核心栈等。

              

          全部的上下文信息组成了一个进程的运行环境。当发生进程调度时,必须对全部上下文信息进行切换,新调度的进程才能运行。进程就是上下文的集合的一个抽象概念。

              

       

              

    6.         

    7. 系统调用的功能和分类
    8.     

    操作系统核心在运行期间的活动可以分为两个部分:上半部分(top half)和下半部分(bottom half),其中上半部分为应用程序提供系统调用或自陷的服务,是同步服务,由当前执行的进程引起,在当前进程上下文中执行并允许直接访问当前进程的数据结构;而下半部分则是由处理硬件中断的子程序,是属于异步活动,这些子程序的调用和执行与当前进程无关。上半部分允许被阻塞,因为这样阻塞的是当前进程;下半部分不允许被阻塞,因为阻塞下半部分会引起阻塞一个无辜的进程甚至整个核心。

    系统调用可以看作是一个所有Unix/Linux进程共享的子程序库,但是它是在特权方式下运行,可以存取核心数据结构和它所支持的用户级数据。系统调用的主要功能是使用户可以使用操作系统提供的有关设备管理、文件系统、进程控制进程通讯以及存储管理方面的功能,而不必要了解操作系统的内部结构和有关硬件的细节问题,从而减轻用户负担和保护系统以及提高资源利用率。

    系统调用分为两个部分:与文件子系统交互的和进程子系统交互的两个部分。其中和文件子系统交互的部分进一步由可以包括与设备文件的交互和与普通文件的交互的系统调用(open, close, ioctl, create, unlink, . . . );与进程相关的系统调用又包括进程控制系统调用(fork, exit, getpid, . . . ),进程间通讯,存储管理,进程调度等方面的系统调用。
 

2.Linux下系统调用的实现
    (以i386为例说明)
         A.在Linux中系统调用是怎样陷入核心的?
    系统调用在使用时和一般的函数调用很相似,但是二者是有本质性区别的,函数调用不能引起从用户态到核心态的转换,而正如前面提到的,系统调用需要有一个状态转换。

    在每种平台上,都有特定的指令可以使进程的执行由用户态转换为核心态,这种指令称作操作系统陷入(operating system trap)。进程通过执行陷入指令后,便可以在核心态运行系统调用代码。

    在Linux中是通过软中断来实现这种陷入的,在x86平台上,这条指令是int 0x80。也就是说在Linux中,系统调用的接口是一个中断处理函数的特例。具体怎样通过中断处理函数来实现系统调用的入口将在后面详细介绍。

    这样,就需要在系统启动时,对INT 0x80进行一定的初始化,下面将描述其过程:

1.使用汇编子程序setup_idtlinux/arch/i386/kernel/head.S)初始化idt表(中断描述符表),这时所有的入口函数偏移地址都被设为ignore_int

        

              ( setup_idt:        

      lea ignore_int,%edx

              

      movl $(__KERNEL_CS << 16),%eax

              

      movw %dx,%ax /* selector = 0x0010 = cs */

              

      movw $0x8E00,%dx /* interrupt gate – dpl=0, present */

              

      lea SYMBOL_NAME(idt_table),%edi

              

      mov $256,%ecx

              

      rp_sidt:

              

      movl %eax,(%edi)

              

      movl %edx,4(%edi)

              

      addl $8,%edi

              

      dec %ecx

              

      jne rp_sidt

              

      ret

              

      selector = __KERNEL_CS, DPL = 0, TYPE = E, P = 1;

              

      2.Start_kernel()(linux/init/main.c)调用trap_init()(linux/arch/i386/kernel/trap.c)函数设置中断描述符表。在该函数里,实际上是通过调用函数set_system_gate(SYSCALL_VECTOR,&system_call)来完成该项的设置的。其中的SYSCALL_VECTOR就是0x80,而system_call则是一个汇编子函数,它即是中断0x80的处理函数,主要完成两项工作a. 寄存器上下文的保存;b. 跳转到系统调用处理函数。在后面会详细介绍这些内容。

          

 

(补充说明:门描述符

    set_system_gate()是在linux/arch/i386/kernel/trap.S中定义的,在该文件中还定义了几个类似的函数set_intr_gate(), set_trap_gate, set_call_gate()。这些函数都调用了同一个汇编子函数__set_gate(),该函数的作用是设置门描述符。IDT中的每一项都是一个门描述符。

#define _set_gate(gate_addr,type,dpl,addr)

set_gate(idt_table+n,15,3,addr);

    门描述符的作用是用于控制转移,其中会包括选择子,这里总是为__KERNEL_CS(指向GDT中的一项段描述符)、入口函数偏移地址、门访问特权级(DPL)以及类型标识(TYPE)。Set_system_gateDPL3,表示从特权级3(最低特权级)也可以访问该门,type15,表示为386中断门。)

 
 

        

      B.与系统调用相关的数据结构        

      1.系统调用处理函数的函数名的约定

              

          函数名都以“sys_”开头,后面跟该系统调用的名字。例如,系统调用fork()的处理函数名是sys_fork()

              

      asmlinkage int sys_fork(struct pt_regs regs);

              

      (补充关于asmlinkage的说明)

              

       
              2.系统调用号(System Call Number

              

          核心中为每个系统调用定义了一个唯一的编号,这个编号的定义在linux/include/asm/unistd.h中,编号的定义方式如下所示:

              

      #define __NR_exit 1

              

      #define __NR_fork 2

              

      #define __NR_read 3

              

      #define __NR_write 4

              

      . . . . . .

              

          用户在调用一个系统调用时,系统调用号号作为参数传递给中断0x80,而该标号实际上是后面将要提到的系统调用表(sys_call_table)的下标,通过该值可以找到相映系统调用的处理函数地址。

              

       
              3.系统调用表

          

系统调用表的定义方式如下:(linux/arch/i386/kernel/entry.S

ENTRY(sys_call_table)

.long SYMBOL_NAME(sys_ni_syscall)

.long SYMBOL_NAME(sys_exit)

.long SYMBOL_NAME(sys_fork)

.long SYMBOL_NAME(sys_read)

.long SYMBOL_NAME(sys_write)

. . . . . .

系统调用表记录了各个系统调用处理函数的入口地址,以系统调用号为偏移量很容易的能够在该表中找到对应处理函数地址。在linux/include/linux/sys.h中定义的NR_syscalls表示该表能容纳的最大系统调用数,NR_syscalls = 256

 
C.系统调用函数接口是如何转化为陷入命令
 

        

                  如前面提到的,系统调用是通过一条陷入指令进入核心态,然后根据传给核心的系统调用号为索引在系统调用表中找到相映的处理函数入口地址。这里将详细介绍这一过程。        

          我们还是以x86为例说明:

              

          由于陷入指令是一条特殊指令,而且依赖与操作系统实现的平台,如在x86中,这条指令是int 0x80,这显然不是用户在编程时应该使用的语句,因为这将使得用户程序难于移植。所以在操作系统的上层需要实现一个对应的系统调用库,每个系统调用都在该库中包含了一个入口点(如我们看到的fork, open, close等等),这些函数对程序员是可见的,而这些库函数的工作是以对应系统调用号作为参数,执行陷入指令int 0x80,以陷入核心执行真正的系统调用处理函数。当一个进程调用一个特定的系统调用库的入口点,正如同它调用任何函数一样,对于库函数也要创建一个栈帧。而当进程执行陷入指令时,它将处理机状态转换到核心态,并且在核心栈执行核心代码。

              

          这里给出一个示例(linux/include/asm/unistd.h):

              

      #define _syscallN(type, name, type1, arg1, type2, arg2, . . . ) \

              

      type name(type1 arg1,type2 arg2) \

              

      { \

              

      long __res; \

              

      __asm__ volatile ("int $0x80" \

              

      : "=a" (__res) \

              

      : "" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \

              

      . . . . . .

              

      __syscall_return(type,__res); \

              

      }

              

          在执行一个系统调用库中定义的系统调用入口函数时,实际执行的是类似如上的一段代码。这里牵涉到一些gcc的嵌入式汇编语言,不做详细的介绍,只简单说明其意义:

              

          其中__NR_##name是系统调用号,如name == ioctl,则为__NR_ioctl,它将被放在寄存器eax中作为参数传递给中断0x80的处理函数。而系统调用的其它参数arg1, arg2, …则依次被放入ebx, ecx, . . .等通用寄存器中,并作为系统调用处理函数的参数,这些参数是怎样传入核心的将会在后面介绍。

              

          下面将示例说明:

              

      int func1()

              

      {

              

      int fd, retval;

              

      fd = open(filename, ……);

              

      ……

              

      ioctl(fd, cmd, arg);

              

      . . .

              

      }

              

       

              

      func2()

              

      {

              

      int fd, retval;

              

      fd = open(filename, ……);

              

      ……

              

      __asm__ __volatile__(\

              

      "int $0x80\n\t"\

              

      :"=a"(retval)\

              

      :"0"(__NR_ioctl),\

              

      "b"(fd),\

              

      "c"(cmd),\

              

      "d"(arg));

              

      }

              

          这两个函数在Linux/x86上运行的结果应该是一样的。

              

          若干个库函数可以映射到同一个系统调用入口点。系统调用入口点对每个系统调用定义其真正的语法和语义,但库函数通常提供一个更方便的接口。如系统调用exec有集中不同的调用方式:execl, execle,等,它们实际上只是同一系统调用的不同接口而已。对于这些调用,它们的库函数对它们各自的参数加以处理,来实现各自的特点,但是最终都被映射到同一个核心入口点。

              

       D.系统调用陷入内核后作何初始化处理

          

    当进程执行系统调用时,先调用系统调用库中定义某个函数,该函数通常被展开成前面提到的_syscallN的形式通过INT 0x80来陷入核心,其参数也将被通过寄存器传往核心。

    在这一部分,我们将介绍INT 0x80的处理函数system_call

    思考一下就会发现,在调用前和调用后执行态完全不相同:前者是在用户栈上执行用户态程序,后者在核心栈上执行核心态代码。那么,为了保证在核心内部执行完系统调用后能够返回调用点继续执行用户代码,必须在进入核心态时保存时往核心中压入一个上下文层;在从核心返回时会弹出一个上下文层,这样用户进程就可以继续运行。

    那么,这些上下文信息是怎样被保存的,被保存的又是那些上下文信息呢?这里仍以x86为例说明。

    在执行INT指令时,实际完成了以下几条操作:

1.由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的核心堆栈信息(SSESP);2.把低优先级堆栈信息(SSESP)保留到高优先级堆栈(即核心栈)中;
3.把EFLAGS,外层CSEIP推入高优先级堆栈(核心栈)中。
4.通过IDT加载CSEIP(控制转移至中断处理函数)

然后就进入了中断0x80的处理函数system_call了,在该函数中首先使用了一个宏SAVE_ALL,该宏的定义如下所示:

#define SAVE_ALL \

cld; \

pushl %es; \

pushl %ds; \

pushl %eax; \

pushl %ebp; \

pushl %edi; \

pushl %esi; \

pushl %edx; \

pushl %ecx; \

pushl %ebx; \

movl $(__KERNEL_DS),%edx; \

movl %edx,%ds; \

movl %edx,%es;

    该宏的功能一方面是将寄存器上下文压入到核心栈中,对于系统调用,同时也是系统调用参数的传入过程,因为在不同特权级之间控制转换时,INT指令不同于CALL指令,它不会将外层堆栈的参数自动拷贝到内层堆栈中。所以在调用系统调用时,必须先象前面的例子里提到的那样,把参数指定到各个寄存器中,然后在陷入核心之后使用SAVE_ALL把这些保存在寄存器中的参数依次压入核心栈,这样核心才能使用用户传入的参数。下面给出system_call的源代码:

ENTRY(system_call)

pushl %eax # save orig_eax

SAVE_ALL

GET_CURRENT(%ebx)

cmpl $(NR_syscalls),%eax

jae badsys

testb $0x20,flags(%ebx) # PF_TRACESYS

jne tracesys

call *SYMBOL_NAME(sys_call_table)(,%eax,4)

. . . . . .

          在这里所做的所有工作是:
           1.保存EAX寄存器,因为在SAVE_ALL中保存的EAX寄存器会被调用的返回值所覆盖;
           2.调用SAVE_ALL保存寄存器上下文;
           3.判断当前调用是否是合法系统调用(EAX是系统调用号,它应该小于NR_syscalls);
           4.如果设置了PF_TRACESYS标志,则跳转到syscall_trace,在那里将会把当前进程挂起并向其父进程发送SIGTRAP,这主要是为了设              置调试断点而设计的;
           5.如果没有设置PF_TRACESYS标志,则跳转到该系统调用的处理函数入口。这里是以EAX(即前面提到的系统调用号)作为偏移,在系             统调用表sys_call_table中查找处理函数入口地址,并跳转到该入口地址。
 

补充说明:
1.GET_CURRENT

        

              #define GET_CURRENT(reg)        

      movl %esp, reg;

              

      andl $-8192, reg;

              

          其作用是取得当前进程的task_struct结构的指针返回到reg中,因为在Linux中核心栈的位置是task_struct之后的两个页面处(8192bytes),所以此处把栈指针与-8192则得到的是task_struct结构指针,而task_struct中偏移为4的位置是成员flags,在这里指令testb $0x20,flags(%ebx)检测的就是task_struct->flags

              


              2.堆栈中的参数

              

          正如前面提到的,SAVE_ALL是系统调用参数的传入过程,当执行完SAVE_ALL并且再由CALL指令调用其处理函数时,堆栈的结构应该如上图所示。这时的堆栈结构看起来和执行一个普通带参数的函数调用是一样的,参数在堆栈中对应的顺序是(arg1 ebx),(arg2, ecx,arg3, edx. . . . . .,这正是SAVE_ALL压栈的反顺序,这些参数正是用户在使用系统调用时试图传送给核心的参数。下面是在核心的调用处理函数中使用参数的两种典型方法:

              

      asmlinkage int sys_fork(struct pt_regs regs)

              

      asmlinkage int sys_open(const char * filename, int flags, int mode)

              

          在sys_fork中,把整个堆栈中的内容视为一个struct pt_regs类型的参数,该参数的结构和堆栈的结构是一致的,所以可以使用堆栈中的全部信息。而在sys_open中参数filename, flags, mode正好对应与堆栈中的ebx, ecx, edx的位置,而这些寄存器正是用户在通过C库调用系统调用时给这些参数指定的寄存器。

              

      __asm__ __volatile__(\

              

      "int $0x80\n\t"\

              

      :"=a"(retval)\

              

      :"0"(__NR_open),\

              

      "b"(filename),\

              

      "c"(flags),\

              

      "d"(mode));

              

       
              3.核心如何使用用户空间的参数

          

        

    

在使用系统调用时,有些参数是指针,这些指针所指向的是用户空间DS寄存器的段选择子所描述段中的地址,而在2.2之前的版本中,核心态的DS段寄存器的中的段选择子和用户态的段选择子描述的段地址不同(前者为0xC0000000, 后者为0x00000000),这样在使用这些参数时就不能读取到正确的位置。所以需要通过特殊的核心函数(如:memcpy_fromfs, mencpy_tofs)来从用户空间数据段读取参数,在这些函数中,是使用FS寄存器来作为读取参数的段寄存器的,FS寄存器在系统调用进入核心态时被设成了USER_DSDS被设成了KERNEL_DS)。在2.2之后的版本用户态和核心态使用的DS中段选择子描述的段地址是一样的(都是0x00000000),所以不需要再经过上面那样烦琐的过程而直接使用参数了。2.2及以后的版本linux/arch/i386/head.S    

ENTRY(gdt_table)

    

.quad 0x0000000000000000/* NULL descriptor */    

.quad 0x0000000000000000/* not used */

    

.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */

    

.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */

    

.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */

    

.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */

    

                           2.0 linux/arch/i386/head.S ENTRY(gdt) .quad 0x0000000000000000 /* NULL descriptor */    

.quad 0x0000000000000000 /* not used */

    

.quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */

    

.quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 */

    

.quad 0x00cbfa000000ffff /* 0x23 user 3GB code at 0x00000000 */

    

.quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 *

    

 

    

2.0版的内核中SAVE_ALL宏定义还有这样几条语句:

    

"movl $" STR(KERNEL_DS) ",%edx\n\t"

    

"mov %dx,%ds\n\t"

    

"mov %dx,%es\n\t"

    

"movl $" STR(USER_DS) ",%edx\n\t"

    

"mov %dx,%fs\n\t"

    

"movl $0,%edx\n\t"

    

 
     

    

E.调用返回
    调用返回的过程要做的工作比其响应过程要多一些,这些工作几乎是每次从核心态返回用户态都需要做的,这里将简要的说明:    

1.判断有没有软中断,如果有则跳转到软中断处理;
    2.判断当前进程是否需要重新调度,如果需要则跳转到调度处理;
    3.如果当前进程有挂起的信号还没有处理,则跳转到信号处理;
    4.使用用RESTORE_ALL来弹出所有被SAVE_ALL压入核心栈的内容并且使用iret返回用户态。

    

F.实例介绍

    

    前面介绍了系统调用相关的数据结构以及在Linux中使用一个系统调用的过程中每一步是怎样处理的,下面将把前面的所有概念串起来,说明怎样在Linux中增加一个系统调用。    

这里实现的系统调用hello仅仅是在控制台上打印一条语句,没有任何功能。

    

1.修改linux/include/i386/unistd.h,在里面增加一条语句:

    

    

            

                  #define __NR_hello ???(这个数字可能因为核心版本不同而不同)
                  2.在某个合适的目录中(如:linux/kernel)增加一个hello.c,修改该目录下的Makefile(把相映的.o文件列入Makefile中就可以了)。
                  3.编写hello.c            

      . . . . . .

                  

      asmlinkage int sys_hello(char * str)

                  

      {

                  

      printk(“My syscall: hello, I know what you say to me: %s ! \n”, str);

                  

      return 0;

                  

      }

                  

       
                  4.修改linux/arch/i386/kernel/entry.S,在里面增加一条语句:

                  

      ENTRY(sys_call_table)

                  

      . . . . . .

                  

      .long SYMBOL_NAME(sys_hello)

                  

      并且修改:

                  

      .rept NR_syscalls-??? /* ??? = ??? +1 */

                  

      .long SYMBOL_NAME(sys_ni_syscall)
                  5.在linux/include/i386/中增加hello.h,里面至少应包括这样几条语句:

              

        

    

#include <linux/unistd.h>    

 

    

#ifdef __KERNEL

    

#else

    

inline _syscall1(int, hello, char *, str);

    

#endif

    

这样就可以使用系统调用hello

    

 
     

    

                                                                           Back

    

 

    

    

            

  • Linux中的系统调用
  •     

    

    1. 进程相关的系统调用
            Fork & vfork & clone        

        进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合,这些资源在Linux中被抽象成各种数据对象:进程控制块、虚存空间、文件系统,文件I/O、信号处理函数。所以创建一个进程的过程就是这些数据对象的创建过程。

            

        在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipefifoSystem V IPC机制等,另外通过fork创建子进程系统开销很大,需要将上面描述的每种资源都复制一个副本。这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程(由于Linux中是采取了copy-on-write技术,所以这一步骤的所做的工作只是虚存管理部分的复制以及页表的创建,而并没有包括物理也面的拷贝);另外,有时一个进程中具有几个独立的计算单元,可以在相同的地址空间上基本无冲突进行运算,但是为了把这些计算单元分配到不同的处理器上,需要创建几个子进程,然后各个子进程分别计算最后通过一定的进程间通讯和同步机制把计算结果汇总,这样做往往有许多格外的开销,而且这种开销有时足以抵消并行计算带来的好处。

            

     

            

        这说明了把计算单元抽象到进程上是不充分的,这也就是许多系统中都引入了线程的概念的原因。在讲述线程前首先介绍以下vfork系统调用,vfork系统调用不同于fork,用vfork创建的子进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,子进程对虚拟地址空间任何数据的修改同样为父进程所见。但是用vfork创建子进程后,父进程会被阻塞直到子进程调用execexit。这样的好处是在子进程被创建后仅仅是为了调用exec执行另一个程序时,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的,通过vfork可以减少不必要的开销。

            

        在Linux中, forkvfork都是调用同一个核心函数

            

        do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)

            

        其中clone_flag包括CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_PIDCLONE_VFORK等等标志位,任何一位被置1了则表明创建的子进程和父进程共享该位对应的资源。所以在vfork的实现中,cloneflags = CLONE_VFORK | CLONE_VM | SIGCHLD,这表示子进程和父进程共享地址空间,同时do_fork会检查CLONE_VFORK,如果该位被置1了,子进程会把父进程的地址空间锁住,直到子进程退出或执行exec时才释放该锁。

            

     

            

        在讲述clone系统调用前先简单介绍线程的一些概念。

            

        线程是在进程的基础上进一步的抽象,也就是说一个进程分为两个部分:线程集合和资源集合。线程是进程中的一个动态对象,它应该是一组独立的指令流,进程中的所有线程将共享进程里的资源。但是线程应该有自己的私有对象:比如程序计数器、堆栈和寄存器上下文。

            

        线程分为三种类型:

            

        内核线程、轻量级进程和用户线程。

            

    内核线程:

            

        它的创建和撤消是由内核的内部需求来决定的,用来负责执行一个指定的函数,一个内核线程不需要一个用户进程联系起来。它共享内核的正文段核全局数据,具有自己的内核堆栈。它能够单独的被调度并且使用标准的内核同步机制,可以被单独的分配到一个处理器上运行。内核线程的调度由于不需要经过态的转换并进行地址空间的重新映射,因此在内核线程间做上下文切换比在进程间做上下文切换快得多。

            

    轻量级进程:

            

        轻量级进程是核心支持的用户线程,它在一个单独的进程中提供多线程控制。这些轻量级进程被单独的调度,可以在多个处理器上运行,每一个轻量级进程都被绑定在一个内核线程上,而且在它的生命周期这种绑定都是有效的。轻量级进程被独立调度并且共享地址空间和进程中的其它资源,但是每个LWP都应该有自己的程序计数器、寄存器集合、核心栈和用户栈。

            

    用户线程:

            

        用户线程是通过线程库实现的。它们可以在没有内核参与下创建、释放和管理。线程库提供了同步和调度的方法。这样进程可以使用大量的线程而不消耗内核资源,而且省去大量的系统开销。用户线程的实现是可能的,因为用户线程的上下文可以在没有内核干预的情况下保存和恢复。每个用户线程都可以有自己的用户堆栈,一块用来保存用户级寄存器上下文以及如信号屏蔽等状态信息的内存区。库通过保存当前线程的堆栈和寄存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下文切换。

            

        内核仍然负责进程的切换,因为只有内核具有修改内存管理寄存器的权力。用户线程不是真正的调度实体,内核对它们一无所知,而只是调度用户线程下的进程或者轻量级进程,这些进程再通过线程库函数来调度它们的线程。当一个进程被抢占时,它的所有用户线程都被抢占,当一个用户线程被阻塞时,它会阻塞下面的轻量级进程,如果进程只有一个轻量级进程,则它的所有用户线程都会被阻塞。

            

     

            

        明确了这些概念后,来讲述Linux的线程和clone系统调用。

            

        在许多实现了MT的操作系统中(如:SolarisDigital Unix等), 线程和进程通过两种数据结构来抽象表示: 进程表项和线程表项,一个进程表项可以指向若干个线程表项, 调度器在进程的时间片内再调度线程。  但是在Linux中没有做这种区分,  而是统一使用task_struct来管理所有进程/线程,只是线程与线程之间的资源是共享的,这些资源可是是前面提到过的:虚存、文件系统、文件I/O以及信号处理函数甚至PID中的几种。

            


                也就是说Linux中,每个线程都有一个task_struct,所以线程和进程可以使用同一调度器调度。其实Linux核心中,轻量级进程和进程没有质上的差别,因为Linux中进程的概念已经被抽象成了计算状态加资源的集合,这些资源在进程间可以共享。如果一个task独占所有的资源,则是一个HWP,如果一个task和其它task共享部分资源,则是LWP

            

        clone系统调用就是一个创建轻量级进程的系统调用:

            

        int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);

            

        其中fn是轻量级进程所执行的过程,stack是轻量级进程所使用的堆栈,flags可以是前面提到的CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID的组合。Clone forkvfork在实现时都是调用核心函数do_fork

            

        do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)

            

        和forkvfork不同的是,forkclone_flag = SIGCHLD

            

        vforkclone_flag = CLONE_VM | CLONE_VFORK | SIGCHLD

            

        而在clone中,clone_flag由用户给出。

            

        下面给出一个使用clone的例子。

            

        Void * func(int arg)

            

        {

            

        . . . . . .

            

        }

            

        int main()

            

        {

            

    int clone_flag, arg;

            

    . . . . . .

            

    clone_flag = CLONE_VM | CLONE_SIGHAND | CLONE_FS |

            

    CLONE_FILES;

            

    stack = (char *)malloc(STACK_FRAME);

            

    stack += STACK_FRAME;

            

    retval = clone((void *)func, stack, clone_flag, arg);

            

    . . . . . .

            

    }

            

        看起来clone的用法和pthread_create有些相似,两者的最根本的差别在于clone是创建一个LWP,对核心是可见的,由核心调度,而pthread_create通常只是创建一个用户线程,对核心是不可见的,由线程库调度。

            

     

            

    Nanosleep & sleep

            

        sleep和nanosleep都是使进程睡眠一段时间后被唤醒,但是二者的实现完全不同。

            

        Linux中并没有提供系统调用sleepsleep是在库函数中实现的,它是通过调用alarm来设定报警时间,调用sigsuspend将进程挂起在信号SIGALARM上,sleep只能精确到秒级上。

            

        nanosleep则是Linux中的系统调用,它是使用定时器来实现的,该调用使调用进程睡眠,并往定时器队列上加入一个time_list型定时器,time_list结构里包括唤醒时间以及唤醒后执行的函数,通过nanosleep加入的定时器的执行函数仅仅完成唤醒当前进程的功能。系统通过一定的机制定时检查这些队列(比如通过系统调用陷入核心后,从核心返回用户态前,要检查当前进程的时间片是否已经耗尽,如果是则调用schedule()函数重新调度,该函数中就会检查定时器队列,另外慢中断返回前也会做此检查),如果定时时间已超过,则执行定时器指定的函数唤醒调用进程。当然,由于系统时间片可能丢失,所以nanosleep精度也不是很高。

            

        alarm也是通过定时器实现的,但是其精度只精确到秒级,另外,它设置的定时器执行函数是在指定时间向当前进程发送SIGALRM信号。

            

     
            2.存储相关的系统调用

        

    

mmap:文件映射    

    在讲述文件映射的概念时,不可避免的要牵涉到虚存(SVR 4VM)。实际上,文件映射是虚存的中心概念,文件映射一方面给用户提供了一组措施,似的用户将文件映射到自己地址空间的某个部分,使用简单的内存访问指令读写文件;另一方面,它也可以用于内核的基本组织模式,在这种模式种,内核将整个地址空间视为诸如文件之类的一组不同对象的映射。

    

    Unix中的传统文件访问方式是,首先用open系统调用打开文件,然后使用readwrite以及lseek等调用进行顺序或者随即的I/O。这种方式是非常低效的,每一次I/O操作都需要一次系统调用。另外,如果若干个进程访问同一个文件,每个进程都要在自己的地址空间维护一个副本,浪费了内存空间。而如果能够通过一定的机制将页面映射到进程的地址空间中,也就是说首先通过简单的产生某些内存管理数据结构完成映射的创建。当进程访问页面时产生一个缺页中断,内核将页面读入内存并且更新页表指向该页面。而且这种方式非常方便于同一副本的共享。

    

    下面给出以上两种方式的对比图:

    

 

    

    VM是面向对象的方法设计的,这里的对象是指内存对象:内存对象是一个软件抽象的概念,它描述内存区与后备存储之间的映射。系统可以使用多种类型的后备存储,比如交换空间,本地或者远程文件以及帧缓存等等。VM系统对它们统一处理,采用同一操作集操作,比如读取页面或者回写页面等。每种不同的后备存储都可以用不同的方法实现这些操作。这样,系统定义了一套统一的接口,每种后备存储给出自己的实现方法。

    

    这样,进程的地址空间就被视为一组映射到不同数据对象上的的映射组成。所有的有效地址就是那些映射到数据对象上的地址。这些对象为映射它的页面提供了持久性的后备存储。映射使得用户可以直接寻址这些对象。

    

    值得提出的是,VM体系结构独立于Unix系统,所有的Unix系统语义,如正文,数据及堆栈区都可以建构在基本VM系统之上。同时,VM体系结构也是独立于存储管理的,存储管理是由操作系统实施的,如:究竟采取什么样的对换和请求调页算法,究竟是采取分段还是分页机制进行存储管理,究竟是如何将虚拟地址转换成为物理地址等等(Linux中是一种叫Three Level Page Table的机制),这些都与内存对象的概念无关。

    

    下面介绍LinuxVM的实现。

    

    如下图所示,一个进程应该包括一个mm_structmemory manage struct),该结构是进程虚拟地址空间的抽象描述,里面包括了进程虚拟空间的一些管理信息:start_code, end_code, start_data, end_data, start_brk, end_brk等等信息。另外,也有一个指向进程虚存区表(vm_area_struct virtual memory area)的指针,该链是按照虚拟地址的增长顺序排列的。

    


        在Linux进程的地址空间被分作许多区(vma),每个区(vma)都对应虚拟地址空间上一段连续的区域,vma是可以被共享和保护的独立实体,这里的vma就是前面提到的内存对象。这里给出vm_area_struct的结构,其中,前半部分是公共的,与类型无关的一些数据成员,如:指向mm_struct的指针,地址范围等等,后半部分则是与类型相关的成员,其中最重要的是一个指向vm_operation_struct向量表的指针vm_opsvm_pos向量表是一组虚函数,定义了与vma类型无关的接口。每一个特定的子类,即每种vma类型都必须在向量表中实现这些操作。这里包括了:open, close, unmap, protect, sync, nopage, wppage, swapout这些操作。

    

struct vm_area_struct {

    

/*公共的,与vma类型无关的 */

    

struct mm_struct * vm_mm;    

unsigned long vm_start;

    

unsigned long vm_end;

    

struct vm_area_struct *vm_next;

    

pgprot_t vm_page_prot;

    

unsigned long vm_flags;

    

short vm_avl_height;

    

struct vm_area_struct * vm_avl_left;

    

struct vm_area_struct * vm_avl_right;

    

struct vm_area_struct *vm_next_share;

    

struct vm_area_struct **vm_pprev_share;

    

/* 与类型相关的 */

    

struct vm_operations_struct * vm_ops;

    

unsigned long vm_pgoff;

    

struct file * vm_file;

    

unsigned long vm_raend

    

void * vm_private_data;

    

};    

vm_ops: open, close, no_page, swapin, swapout . . . . . .

    

    介绍完VM的基本概念后,我们可以讲述mmap, munmap系统调用了。mmap调用实际上就是一个内存对象vma的创建过程,mmap的调用格式是: void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);其中start是映射地址,length是映射长度,如果flagsMAP_FIXED不被置位,则该参数通常被忽略,而查找进程地址空间中第一个长度符合的空闲区域;Fd是映射文件的文件句柄,offset是映射文件中的偏移地址;prot是映射保护权限,可以是PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONEflags则是指映射类型,可以是MAP_FIXED, MAP_PRIVATE, MAP_SHARED,该参数必须被指定为MAP_PRIVATEMAP_SHARED其中之一,MAP_PRIVATE是创建一个写时拷贝映射(copy-on-write),也就是说如果有多个进程同时映射到一个文件上,映射建立时只是共享同样的存储页面,但是某进程企图修改页面内容,则复制一个副本给该进程私用,它的任何修改对其它进程都不可见。而MAP_SHARED则无论修改与否都使用同一副本,任何进程对页面的修改对其它进程都是可见的。    

Mmap系统调用的实现过程是:

    

    1.先通过文件系统定位要映射的文件;
        2.权限检查,映射的权限不会超过文件打开的方式,也就是说如果文件是以只读方式打开,那么则不允许建立一个可写映射;
        3.创建一个vma对象,并对之进行初始化;
        4.调用映射文件的mmap函数,其主要工作是给vm_ops向量表赋值;
        5.把该vma链入该进程的vma链表中,如果可以和前后的vma合并则合并;
        6.如果是要求VM_LOCKED(映射区不被换出)方式映射,则发出缺页请求,把映射页面读入内存中;

    

munmap(void * start, size_t length)    

    该调用可以看作是mmap的一个逆过程。它将进程中从start开始length长度的一段区域的映射关闭,如果该区域不是恰好对应一个vma,则有可能会分割几个或几个vma

    

Msync(void * start, size_t length, int flags)

    

    把映射区域的修改回写到后备存储中。因为munmap时并不保证页面回写,如果不调用msync,那么有可能在munmap后丢失对映射区的修改。其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATEMS_SYNC要求回写完成后才返回,MS_ASYNC发出回写请求后立即返回,MS_INVALIDATE使用回写的内容更新该文件的其它映射。

    

    该系统调用是通过调用映射文件的sync函数来完成工作的。

    

brk(void * end_data_segement):

    

    将进程的数据段扩展到end_data_segement指定的地址,该系统调用和mmap的实现方式十分相似,同样是产生一个vma,然后指定其属性。不过在此之前需要做一些合法性检查,比如该地址是否大于mm->end_codeend_data_segementmm->brk之间是否还存在其它vma等等。通过brk产生的vma映射的文件为空,这和匿名映射产生的vma相似,关于匿名映射不做进一步介绍。我们使用的库函数malloc就是通过brk实现的,通过下面这个例子很容易证实这点:

    

main()

    

{

    

char * m, * n;

    

int size;

    

 

    

m = (char *)sbrk(0);

    

printf("sbrk addr = %08lx\n", m);

    

do {

    

n = malloc(1024);

    

printf("malloc addr = %08lx\n", n);

    

}w hile(n < m);    

m = (char *)sbrk(0);

    

printf("new sbrk addr = %08lx\n", m);    

}

    

 

    

       sbrk addr = 0804a000 malloc addr = 080497d8    

malloc addr = 08049be0

    

malloc addr = 08049fe8

    

malloc addr = 0804a3f0

    

new sbrk addr = 0804b000

    

3.进程间通信(IPC

    

    

                进程间通讯可以通过很多种机制,包括signal, pipe, fifo, System V IPC, 以及socket等等,前几种概念都比较好理解,这里着重介绍关于System V IPC        

        System V IPC包括三种机制:message(允许进程发送格式化的数据流到任意的进程)、shared memory(允许进程间共享它们虚拟地址空间的部分区域)和semaphore(允许进程间同步的执行)。

            

        操作系统核心中为它们分别维护着一个表,这三个表是系统中所有这三种IPC对象的集合,表的索引是一个数值ID,进程通过这个ID可以查找到需要使用的IPC资源。进程每创建一个IPC对象,系统中都会在相应的表中增加一项。之后其它进程(具有许可权的进程)只要通过该IPC对象的ID则可以引用它。

            

        IPC对象必须使用IPC_RMID命令来显示的释放,否则这个对象就处于活动状态,甚至所有的使用它的进程都已经终止。这种机制某些时候十分有用,但是也正因为这种特征,使得操作系统内核无法判断IPC对象是被用户故意遗留下来供将来其它进程使用还是被无意抛弃的。

            

        Linux中只提供了一个系统调用接口ipc()来完成所有System V IPC操作,我们常使用的是建立在该调用之上的库函数接口。对于这三种IPC,都有很相似的三种调用:xxxget, (msgsnd, msgrcv)semopt | (shmat, shmdt), xxxctl

            

        Xxxget:获取调用,在系统中申请或者查询一个IPC资源,返回值是该IPC对象的ID,该调用类似于文件系统的open, create调用;

            

        Xxxctl:控制调用,至少包括三种操作:XXX_RMID(释放IPC对象), XXX_STAT(查询状态), XXX_SET(设置状态信息);

            

        (msgsnd, msgrcv) | Semopt | (shmat, shmdt)|:操作调用,这些调用的功能随IPC对象的类型不同而有较大差异。

            

    4.文件系统相关的调用

            

        文件是用来保存数据的,而文件系统则可以让用户组织,操纵以及存取不同的文件。内核允许用户通过一个严格定义的过程性接口与文件系统进行交互,这个接口对用户屏蔽了文件系统的细节,同时指定了所有相关系统调用的行为和语义。Linux支持许多中文件系统,如ext2msdos, ntfs, proc, dev, ufs, nfs等等,这些文件系统都实现了相同的接口,因此给应用程序提供了一致性的视图。但每种文件系统在实现时可能对某个方面加以了一定的限制。如:文件名的长度,是否支持所有的文件系统接口调用。

            

        为了支持多文件系统,sun提出了一种vnode/vfs接口,SVR4中将之实现成了一种工业标准。而Linux作为一种Unixclone体,自然也实现了这种接口,只是它的接口定义和SVR4的稍有不同。Vnode/Vfs接口的设计体现了面向对象的思想,Vfs(虚拟文件系统)代表内核中的一个文件系统,Vnode(虚拟节点)代表内核中的一个文件,它们都可以被视为抽象基类,并可以从中派生出不同的子类以实现不同的文件系统。

            

        由于篇幅原因,这里只是大概的介绍一下怎样通过Vnode/Vfs结构来实现文件系统和访问文件。

            

        在Linux中支持的每种文件系统必须有一个file_system_type结构,此结构的核心是read_super函数,该函数将读取文件系统的超级块。Linux中支持的所有文件系统都会被注册在一条file_system_type结构链中,注册是在系统初始化时调用regsiter_filesystem()完成,如果文件系统是以模块的方式实现,则是在调用init_module时完成。

            


                当mount某种块设备时,将调用系统调用mount,该调用中将会首先检查该类文件系统是否注册在系统种中,如果注册了则先给该文件系统分配一个super_block,并进行初始化,最后调用这种文件系统的read_super函数来完成super_block结构私有数据的赋值。其中最主要的工作是给super_blocks_ops赋值,s_ops是一个函数向量表,由文件系统各自实现了一组操作。

            

    struct super_operations {

            

    void (*read_inode) (struct inode *);

            

    void (*write_inode) (struct inode *);

            

    void (*put_inode) (struct inode *);

            

    void (*delete_inode) (struct inode *);

            

    void (*put_super) (struct super_block *);

            

    void (*write_super) (struct super_block *);

            

    int (*statfs) (struct super_block *, struct statfs *);

            

    int (*remount_fs) (struct super_block *, int *, char *);

            

    void (*clear_inode) (struct inode *);

            

    void (*umount_begin) (struct super_block *);

            

    };

            

        由于这组操作中定义了文件系统中对于inode的操作,所以是之后对于文件系统中文件所有操作的基础。

            

        在给super_blocks_ops赋值后,再给该文件系统分配一个vfsmount结构,将该结构注册到系统维护的另一条链vfsmntlist中,所有mount上的文件系统都在该链中有一项。在umount时,则从链中删除这一项并且释放超级块。

            

        对于一个已经mount的文件系统中任何文件的操作首先应该以产生一个inode实例,即根据文件系统的类型生成一个属于该文件系统的内存i节点。这首先调用文件定位函数lookup_dentry查找目录缓存看是否使用过该文件,如果还没有则缓存中找不到,于是需要的i接点则依次调用路径上的所有目录I接点的lookup函数,在lookup函数中会调用iget函数,该函数中最终调用超级块的s_ops->read_inode读取目标文件的磁盘I节点(这一步再往下就是由设备驱动完成了,通过调用驱动程序的read函数读取磁盘I节点),read_inode函数的主要功能是初始化inode的一些私有数据(比如数据存储位置,文件大小等等)以及给inode_operations函数开关表赋值,最终该inode被绑定在一个目录缓存结构dentry中返回。

            

        在获得了文件的inode之后,对于该文件的其它一切操作都有了根基。因为可以从inode 获得文件操作函数开关表file_operatoins,该开关表里给出了标准的文件I/O接口的实现,包括read, write, lseek, mmap, ioctl等等。这些函数入口将是所有关于文件的系统调用请求的最终处理入口,通过这些函数入口会向存储该文件的硬设备驱动发出请求并且由驱动程序返回数据。当然这中间还会牵涉到一些关于buffer的管理问题,这里就不赘述了。

            

        通过讲述这些,我们应该明白了为什么可以使用统一的系统调用接口来访问不同文件系统类型的文件了:因为在文件系统的实现一层,都把低层的差异屏蔽了,用户可见的只是高层可见的一致的系统调用接口。

            

     
            5.与module相关的系统调用

        

    

    Linux中提供了一种动态加载或卸载内核组件的机制——模块。通过这种机制Linux用户可以为自己可以保持一个尽量小的内核映像文件,另外,往内核中加载和卸载模块不需要重新编译整个内核以及引导机器。可以通过一定的命令或者调用在一个运行的系统中加载模块,在不需要时卸载模块。模块可以完成许多功能,比如文件系统、设备驱动,系统支持的执行文件格式,甚至系统调用和中断处理都可以用模块来更新。    

    Linux中提供了往系统中添加和卸载模块的接口,create_module()init_module (), delete_module(),这些系统调用通常不是直接为程序员使用的,它们仅仅是为实现一些系统命令而提供的接口,如insmod, rmmod,(在使用这些系统调用前必须先加载目标文件到用户进程的地址空间,这必须由目标文件格式所特定的库函数(如:libobj.a中的一些函数)来完成)。

    

    Linux的核心中维护了一个module_list列表,每个被加载到核心中的模块都在其中占有一项,系统调用create_module()就是在该列表里注册某个指定的模块,而init_module则是使用模块目标文件内容的映射来初始化核心中注册的该模块,并且调用该模块的初始化函数,初始化函数通常完成一些特定的初始化操作,比如文件系统的初始化函数就是在操作系统中注册该文件系统。delete_module则是从系统中卸载一个模块,其主要工作是从module_list中删除该模块对应的module结构并且调用该模块的cleanup函数卸载其它私有信息。
     
     

    

                                                                                                       Back

    

    

            

  • Linux中怎样编译和定制内核
  •     

    

    1.编译内核前注意的事项        

        检查系统上其它资源是否符合新内核的要求。在linux/Document目录下有一个叫Changes的文件,里面列举了当前内核版本所需要的其它软件的版本号,

            

    – Kernel modutils             2.1.121                           ; insmod -V

            

    – Gnu C                       2.7.2.3                           ; gcc –version

            

    – Binutils                    2.8.1.0.23                        ; ld -v

            

    – Linux libc5 C Library       5.4.46                            ; ls -l /lib/libc*

            

    – Linux libc6 C Library       2.0.7pre6                         ; ls -l /lib/libc*

            

    – Dynamic Linker (ld.so)      1.9.9                             ; ldd –version or ldd -v

            

    – Linux C++ Library           2.7.2.8                           ; ls -l /usr/lib/libg++.so.*

            

    . . . . . .

            

    其中最后一项是列举该软件版本号的命令,如果不符合要求先给相应软件升级,这一步通常可以忽略。

            

    2.配置内核

            

        使用make config或者make menuconfig, make xconfig配置新内核。其中包括选择块设备驱动程序、网络选项、网络设备支持、文件系统等等,用户可以根据自己的需求来进行功能配置。每个选项至少有“y”和“n”两种选择,选择“y”表示把相应的支持编译进内核,选“n”表示不提供这种支持,还有的有第三种选择“m”,则表示把该支持编译成可加载模块,即前面提到的module,怎样编译和安装模块在后面会介绍。

            

        这里,顺便讲述一下如何在内核中增加自己的功能支持。

            

        假如我们现在需要在自己的内核中加入一个文件系统tfile,在完成了文件系统的代码后,在linux/fs下建立一个tfile目录,把源文件拷贝到该目录下,然后修改linux/fs下的Makefile,把对应该文件系统的目标文件加入目标文件列表中,最后修改linux/fs/Config.in文件,加入

            

    bool ‘tfile fs support’ CONFIG_TFILE_FS或者

            

    tristate ‘tfile fs support’ CONFIG_TFILE_FS

            

    这样在Make menuconfig时在filesystem选单下就可以看到

            

    < > tfile fs support一项了

            

    3.编译内核

            

        在配置好内核后就是编译内核了,在编译之前首先应该执行make dep命令建立好依赖关系,该命令将会修改linux中每个子目录下的.depend文件,该文件包含了该目录下每个目标文件所需要的头文件(绝对路径的方式列举)。

            

        然后就是使用make bzImage命令来编译内核了。该命令运行结束后将会在linux/arch/asm/boot/产生一个名叫bzImage的映像文件。

            

    4.使用新内核引导

            

        把前面编译产生的映像文件拷贝到/boot目录下(也可以直接建立一个符号连接,这样可以省去每次编译后的拷贝工作),这里暂且命名为vmlinuz-new,那么再修改/etc/lilo.conf,在其中增加这么几条:

            

    image = /boot/vmlinuz-new

            

    root = /dev/hda1

            

    label = new

            

    read-only

            

    并且运行lilo命令,那么系统在启动时就可以选用新内核引导了。

            

    5.编译模块和使用模块

        

    

linux/目录下执行make modules编译模块,然后使用命令make modules_install来安装模块(所有的可加载模块的目标文件会被拷贝到/lib/modules/2.2.12/),这样之后就可以通过执行insmod 〈模块名〉和rmmod〈模块名〉命令来加载或卸载功能模块了。

php.ini 中的几个参数设置

设置                                   描述                                                                                           建议值
max_execution_time     一个脚本可使用多少 CPU 秒                                                 30
max_input_time              一个脚本等待输入数据的时间有多长(秒)                        60
memory_limit                  在被取消之前,一个脚本可使用多少内存(字节)            32M
output_buffering             数据发送给客户机之前,有多少数据(字节)需要缓存    4096

具体数字主要取决于您的应用程序。如果要从用户处接收大文件,那么 max_input_time 可能必须增加,可以在 php.ini 中修改,也可以通过代码重写它。与之类似,CPU 或内存占用较多的程序也可能需要更大的设置值。目标就是缓解超标程序的影响,因此不建议全局禁用这些设置。关于 max_execution_time,还有一点需要注意:它表示进程的 CPU 时间,而不是绝对时间。因此一个进行大量 I/O 和少量计算的程序的运行时间可能远远超过 max_execution_time。这也是 max_input_time 可以大于 max_execution_time 的原因所在。

关于script 标签的 defer 属性

DEFER Attribute | defer Property

Sets or retrieves the status of the script.

Syntax

HTML             <SCRIPT DEFER … >
Scripting        SCRIPT.defer [ = bDefer ]

Possible Values

    

        

            

            

        

    

bDefer Boolean that specifies or receives one of the following values.            

                

                    

                        

                        

                    

                    

                        

                        

                    

                

            

false Default. Inline executable function is not deferred.
true Inline executable function is deferred.

            

The property is read/write. The property has a default value of false.

Remarks

Using the attribute at design time can improve the download performance of a page because the browser does not need to parse and execute the script and can continue downloading and parsing the page instead.

需要注意的几点:

1.使用了defer="true"属性的script标签应该放在<head></head>之间。如果放在Body之间可能会得不到预期的效果(我试过如果把script放body里,在页面加载的第一次不会起defer的作用,但之后的刷新defer却起作用)。

2.使用了defer="true"属性的script标签里面包含的脚本在运行的过程中,不能使用document.write()方法向页面输出内容。因为设置了defer="true"的脚本是页面加载之后才加载并渲染的.如果这时候使用document.write()方法,会把之前的页面内容都清掉(当然,如果有这个需要的情况除外)。

PHP生成PDF文档的FPDF类

PHP生成PDF文档的FPDF类
以前在PHP4的早期版本中用PDFlib生成PDF文档比较容易,现在升级到PHP5了,发现更麻烦了,装的PHP 5.2.4默认没有PHPlib,从php.net上找了一个,装上竟一直报错,开始以为是版本兼容问题,后来在租来的服务器上(PHP 4.3.11)也是不行,在网上搜索,看到PHPlib居然还是非免费的,算了吧,放弃!
继续搜索其他的解决方案,phpMyAdmin用的有生成PDF的功能,是TCPDF,测试发现不支持中文,所有的汉字都只显示为方格,戒烟如你初步判断为字库问题,网上也没有合适的解决办法,只好再放弃!
最后才找到一个叫FPDF的东西,简单、实用、支持中文,在PHP 5.2.4和PHP 4.3.11上运行均正常,完全符合戒烟如你的要求,吼吼~~就是你了!

  下载:http://www.fpdf.org/
中文包:http://www.fpdf.org/download/chinese.zip
中文手册:http://www.fpdf.org/en/dl.php?id=72
你也可以直接从本文的附件里下载,提供了1.5.2和1.5.3两个版本的FPDF,但中文版的手册只有1.5.2的,说的还满详细,基本可以指导操作了。

  把中文包里的chinese.php和ex.php解压到下载的FPDF包里,运行ex.php,就可以看到繁体中文显示的东西。

  把下面的代码:
$pdf->AddBig5Font();
$pdf->SetFont(‘Big5’,”,20);
替换为:
$pdf->AddGBFont();
$pdf->SetFont(‘GB’,”,20);

  再把$pdf->Write(5,’*’);的*替换成你想输出的中文,就一切OK了!再运行下ex.php试试看?