﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>IT博客-【Z&amp;Y】幸福小筑-文章分类-Linux使用及编程技巧</title><link>http://www.cnitblog.com/drizztzou/category/165.html</link><description>                          天道酬勤
Now its the time for me to work hard for my girl!</description><language>zh-cn</language><lastBuildDate>Tue, 27 Sep 2011 16:54:30 GMT</lastBuildDate><pubDate>Tue, 27 Sep 2011 16:54:30 GMT</pubDate><ttl>60</ttl><item><title>Linux系统调用列表</title><link>http://www.cnitblog.com/drizztzou/articles/3424.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 20 Oct 2005 04:49:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/3424.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/3424.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/3424.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/3424.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/3424.html</trackback:ping><description><![CDATA[<span class="postbody">本文列出了大部分常见的Linux系统调用，并附有简要中文说明。 
<br>
以下是Linux系统调用的一个列表，包含了大部分常用系统调用和由系统调用派生出的的函
<br>
数。这可能是你在互联网上所能看到的唯一一篇中文注释的Linux系统调用列表，即使是简
<br>
单的字母序英文列表，能做到这么完全也是很罕见的。 
<br>

<br>
按照惯例，这个列表以man pages第2节，即系统调用节为蓝本。按照笔者的理解，对其作
<br>
了大致的分类，同时也作了一些小小的修改，删去了几个仅供内核使用，不允许用户调用
<br>
的系统调用，对个别本人稍觉不妥的地方作了一些小的修改，并对所有列出的系统调用附
<br>
上简要注释。 
<br>

<br>
其中有一些函数的作用完全相同，只是参数不同。（可能很多熟悉C++朋友马上就能联想起
<br>
函数重载，但是别忘了Linux核心是用C语言写的，所以只能取成不同的函数名）。还有一
<br>
些函数已经过时，被新的更好的函数所代替了（gcc在链接这些函数时会发出警告），但因
<br>
为兼容的原因还保留着，这些函数我会在前面标上“*”号以示区别。 
<br>

<br>
一、进程控制： 
<br>

<br>
fork 创建一个新进程 
<br>
clone 按指定条件创建子进程 
<br>
execve 运行可执行文件 
<br>
exit 中止进程 
<br>
_exit 立即中止当前进程 
<br>
getdtablesize 进程所能打开的最大文件数 
<br>
getpgid 获取指定进程组标识号 
<br>
setpgid 设置指定进程组标志号 
<br>
getpgrp 获取当前进程组标识号 
<br>
setpgrp 设置当前进程组标志号 
<br>
getpid 获取进程标识号 
<br>
getppid 获取父进程标识号 
<br>
getpriority 获取调度优先级 
<br>
setpriority 设置调度优先级 
<br>
modify_ldt 读写进程的本地描述表 
<br>
nanosleep 使进程睡眠指定的时间 
<br>
nice 改变分时进程的优先级 
<br>
pause 挂起进程，等待信号 
<br>
personality 设置进程运行域 
<br>
prctl 对进程进行特定操作 
<br>
ptrace 进程跟踪 
<br>
sched_get_priority_max 取得静态优先级的上限 
<br>
sched_get_priority_min 取得静态优先级的下限 
<br>
sched_getparam 取得进程的调度参数 
<br>
sched_getscheduler 取得指定进程的调度策略 
<br>
sched_rr_get_interval 取得按RR算法调度的实时进程的时间片长度 
<br>
sched_setparam 设置进程的调度参数 
<br>
sched_setscheduler 设置指定进程的调度策略和参数 
<br>
sched_yield 进程主动让出处理器,并将自己等候调度队列队尾 
<br>
vfork 创建一个子进程，以供执行新程序，常与execve等同时使用 
<br>
wait 等待子进程终止 
<br>
wait3 参见wait 
<br>
waitpid 等待指定子进程终止 
<br>
wait4 参见waitpid 
<br>
capget 获取进程权限 
<br>
capset 设置进程权限 
<br>
getsid 获取会晤标识号 
<br>
setsid 设置会晤标识号 
<br>

<br>
二、文件系统控制 
<br>

<br>
1、文件读写操作 
<br>

<br>
fcntl 文件控制 
<br>
open 打开文件 
<br>
creat 创建新文件 
<br>
close 关闭文件描述字 
<br>
read 读文件 
<br>
write 写文件 
<br>
readv 从文件读入数据到缓冲数组中 
<br>
writev 将缓冲数组里的数据写入文件 
<br>
pread 对文件随机读 
<br>
pwrite 对文件随机写 
<br>
lseek 移动文件指针 
<br>
_llseek 在64位地址空间里移动文件指针 
<br>
dup 复制已打开的文件描述字 
<br>
dup2 按指定条件复制文件描述字 
<br>
flock 文件加/解锁 
<br>
poll I/O多路转换 
<br>
truncate 截断文件 
<br>
ftruncate 参见truncate 
<br>
umask 设置文件权限掩码 
<br>
fsync 把文件在内存中的部分写回磁盘 
<br>

<br>
2、文件系统操作 
<br>

<br>
access 确定文件的可存取性 
<br>
chdir 改变当前工作目录 
<br>
fchdir 参见chdir 
<br>
chmod 改变文件方式 
<br>
fchmod 参见chmod 
<br>
chown 改变文件的属主或用户组 
<br>
fchown 参见chown 
<br>
lchown 参见chown 
<br>
chroot 改变根目录 
<br>
stat 取文件状态信息 
<br>
lstat 参见stat 
<br>
fstat 参见stat 
<br>
statfs 取文件系统信息 
<br>
fstatfs 参见statfs 
<br>
readdir 读取目录项 
<br>
getdents 读取目录项 
<br>
mkdir 创建目录 
<br>
mknod 创建索引节点 
<br>
rmdir 删除目录 
<br>
rename 文件改名 
<br>
link 创建链接 
<br>
symlink 创建符号链接 
<br>
unlink 删除链接 
<br>
readlink 读符号链接的值 
<br>
mount 安装文件系统 
<br>
umount 卸下文件系统 
<br>
ustat 取文件系统信息 
<br>
utime 改变文件的访问修改时间 
<br>
utimes 参见utime 
<br>
quotactl 控制磁盘配额 
<br>

<br>
三、系统控制 
<br>

<br>
ioctl I/O总控制函数 
<br>
_sysctl 读/写系统参数 
<br>
acct 启用或禁止进程记账 
<br>
getrlimit 获取系统资源上限 
<br>
setrlimit 设置系统资源上限 
<br>
getrusage 获取系统资源使用情况 
<br>
uselib 选择要使用的二进制函数库 
<br>
ioperm 设置端口I/O权限 
<br>
iopl 改变进程I/O权限级别 
<br>
outb 低级端口操作 
<br>
reboot 重新启动 
<br>
swapon 打开交换文件和设备 
<br>
swapoff 关闭交换文件和设备 
<br>
bdflush 控制bdflush守护进程 
<br>
sysfs 取核心支持的文件系统类型 
<br>
sysinfo 取得系统信息 
<br>
adjtimex 调整系统时钟 
<br>
alarm 设置进程的闹钟 
<br>
getitimer 获取计时器值 
<br>
setitimer 设置计时器值 
<br>
gettimeofday 取时间和时区 
<br>
settimeofday 设置时间和时区 
<br>
stime 设置系统日期和时间 
<br>
time 取得系统时间 
<br>
times 取进程运行时间 
<br>
uname 获取当前UNIX系统的名称、版本和主机等信息 
<br>
vhangup 挂起当前终端 
<br>
nfsservctl 对NFS守护进程进行控制 
<br>
vm86 进入模拟8086模式 
<br>
create_module 创建可装载的模块项 
<br>
delete_module 删除可装载的模块项 
<br>
init_module 初始化模块 
<br>
query_module 查询模块信息 
<br>
*get_kernel_syms 取得核心符号,已被query_module代替 
<br>

<br>
四、内存管理 
<br>

<br>
brk 改变数据段空间的分配 
<br>
sbrk 参见brk 
<br>
mlock 内存页面加锁 
<br>
munlock 内存页面解锁 
<br>
mlockall 调用进程所有内存页面加锁 
<br>
munlockall 调用进程所有内存页面解锁 
<br>
mmap 映射虚拟内存页 
<br>
munmap 去除内存页映射 
<br>
mremap 重新映射虚拟内存地址 
<br>
msync 将映射内存中的数据写回磁盘 
<br>
mprotect 设置内存映像保护 
<br>
getpagesize 获取页面大小 
<br>
sync 将内存缓冲区数据写回硬盘 
<br>
cacheflush 将指定缓冲区中的内容写回磁盘 
<br>

<br>
五、网络管理 
<br>

<br>
getdomainname 取域名 
<br>
setdomainname 设置域名 
<br>
gethostid 获取主机标识号 
<br>
sethostid 设置主机标识号 
<br>
gethostname 获取本主机名称 
<br>
sethostname 设置主机名称 
<br>

<br>
六、socket控制 
<br>

<br>
socketcall socket系统调用 
<br>
socket 建立socket 
<br>
bind 绑定socket到端口 
<br>
connect 连接远程主机 
<br>
accept 响应socket连接请求 
<br>
send 通过socket发送信息 
<br>
sendto 发送UDP信息 
<br>
sendmsg 参见send 
<br>
recv 通过socket接收信息 
<br>
recvfrom 接收UDP信息 
<br>
recvmsg 参见recv 
<br>
listen 监听socket端口 
<br>
select 对多路同步I/O进行轮询 
<br>
shutdown 关闭socket上的连接 
<br>
getsockname 取得本地socket名字 
<br>
getpeername 获取通信对方的socket名字 
<br>
getsockopt 取端口设置 
<br>
setsockopt 设置端口参数 
<br>
sendfile 在文件或端口间传输数据 
<br>
socketpair 创建一对已联接的无名socket 
<br>

<br>
七、用户管理 
<br>

<br>
getuid 获取用户标识号 
<br>
setuid 设置用户标志号 
<br>
getgid 获取组标识号 
<br>
setgid 设置组标志号 
<br>
getegid 获取有效组标识号 
<br>
setegid 设置有效组标识号 
<br>
geteuid 获取有效用户标识号 
<br>
seteuid 设置有效用户标识号 
<br>
setregid 分别设置真实和有效的的组标识号 
<br>
setreuid 分别设置真实和有效的用户标识号 
<br>
getresgid 分别获取真实的,有效的和保存过的组标识号 
<br>
setresgid 分别设置真实的,有效的和保存过的组标识号 
<br>
getresuid 分别获取真实的,有效的和保存过的用户标识号 
<br>
setresuid 分别设置真实的,有效的和保存过的用户标识号 
<br>
setfsgid 设置文件系统检查时使用的组标识号 
<br>
setfsuid 设置文件系统检查时使用的用户标识号 
<br>
getgroups 获取后补组标志清单 
<br>
setgroups 设置后补组标志清单 
<br>

<br>
八、进程间通信 
<br>

<br>
ipc 进程间通信总控制调用 
<br>

<br>
1、信号 
<br>

<br>
sigaction 设置对指定信号的处理方法 
<br>
sigprocmask 根据参数对信号集中的信号执行阻塞/解除阻塞等操作 
<br>
sigpending 为指定的被阻塞信号设置队列 
<br>
sigsuspend 挂起进程等待特定信号 
<br>
signal 参见signal 
<br>
kill 向进程或进程组发信号 
<br>
*sigblock 向被阻塞信号掩码中添加信号,已被sigprocmask代替 
<br>
*siggetmask 取得现有阻塞信号掩码,已被sigprocmask代替 
<br>
*sigsetmask 用给定信号掩码替换现有阻塞信号掩码,已被sigprocmask代替 
<br>
*sigmask 将给定的信号转化为掩码,已被sigprocmask代替 
<br>
*sigpause 作用同sigsuspend,已被sigsuspend代替 
<br>
sigvec 为兼容BSD而设的信号处理函数,作用类似sigaction 
<br>
ssetmask ANSI C的信号处理函数,作用类似sigaction 
<br>

<br>
2、消息 
<br>

<br>
msgctl 消息控制操作 
<br>
msgget 获取消息队列 
<br>
msgsnd 发消息 
<br>
msgrcv 取消息 
<br>

<br>
3、管道 
<br>

<br>
pipe 创建管道 
<br>

<br>
4、信号量 
<br>

<br>
semctl 信号量控制 
<br>
semget 获取一组信号量 
<br>
semop 信号量操作 
<br>

<br>
5、共享内存 
<br>

<br>
shmctl 控制共享内存 
<br>
shmget 获取共享内存 
<br>
shmat 连接共享内存 
<br>
shmdt 拆卸共享内存
</span><img src ="http://www.cnitblog.com/drizztzou/aggbug/3424.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-10-20 12:49 <a href="http://www.cnitblog.com/drizztzou/articles/3424.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>用C语言实现Ping程序功能</title><link>http://www.cnitblog.com/drizztzou/articles/3419.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Wed, 19 Oct 2005 16:46:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/3419.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/3419.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/3419.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/3419.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/3419.html</trackback:ping><description><![CDATA[<span class="postbody">大部分人用ping命令只是作为查看另一个系统的网络连接是否正常的一种简单方法。在这篇文章中，作者将介绍如何用C语言编写一个模拟ping命令功能的程序。
<br>
ping命令是用来查看网络上另一个主机系统的网络连接是否正常的一个工具。ping命令的工作原理是：向网络上的另一个主机系统发送ICMP报文，如果指定系统得到了报文，它将把报文一模一样地传回给发送者，这有点象潜水艇声纳系统中使用的发声装置。 
<br>
例如，在Linux终端上执行ping　localhost命令将会看到以下结果: 
<br>
PING localhost.localdomain (127.0.0.1) from 127.0.0.1 : 56(84) bytes of data.
<br>
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=0 ttl=255 time=112 usec
<br>
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=255 time=79 usec
<br>
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=2 ttl=255 time=78 usec
<br>
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=3 ttl=255 time=82 usec
<br>

<br>
--- localhost.localdomain ping statistics ---
<br>
4 packets transmitted, 4 packets received, 0% packet loss
<br>
round-trip min/avg/max/mdev = 0.078/0.087/0.112/0.018 ms
<br>

<br>
 
<br>

<br>

<br>
由上面的执行结果可以看到，ping命令执行后显示出被测试系统主机名和相应IP地址、返回给当前主机的ICMP报文顺序号、ttl生存时间和往返时间rtt（单位是毫秒，即千分之一秒）。要写一个模拟ping命令，这些信息有启示作用。
<br>

<br>
要真正了解ping命令实现原理，就要了解ping命令所使用到的TCP/IP协议。
<br>

<br>
ICMP(Internet Control
Message,网际控制报文协议)是为网关和目标主机而提供的一种差错控制机制，使它们在遇到差错时能把错误报告给报文源发方。ICMP协议是IP层的
一个协议，但是由于差错报告在发送给报文源发方时可能也要经过若干子网，因此牵涉到路由选择等问题，所以ICMP报文需通过IP协议来发送。ICMP数据
报的数据发送前需要两级封装：首先添加ICMP报头形成ICMP报文，再添加IP报头形成IP数据报。如下图所示
<br>

<br>
IP报头 
<br>
ICMP报头 
<br>
ICMP数据报 
<br>

<br>

<br>
IP报头格式
<br>由于IP层协议是一种点对点的协议，而非端对端的协议，它提供无连接的数据报服务，没有端口的概念，因此很少使用bind()和connect
()函数，若有使用也只是用于设置IP地址。发送数据使用sendto()函数，接收数据使用recvfrom()函数。IP报头格式如下图： <br>

<br>

<br>

<br>
在Linux中，IP报头格式数据结构(&lt;netinet/ip.h&gt;)定义如下： 
<br>
struct ip
<br>
  {
<br>
#if __BYTE_ORDER == __LITTLE_ENDIAN
<br>
    unsigned int ip_hl:4;       /* header length */
<br>
    unsigned int ip_v:4;        /* version */
<br>
#endif
<br>
#if __BYTE_ORDER == __BIG_ENDIAN
<br>
    unsigned int ip_v:4;        /* version */
<br>
    unsigned int ip_hl:4;       /* header length */
<br>
#endif
<br>
    u_int8_t ip_tos;            /* type of service */
<br>
    u_short ip_len;         /* total length */
<br>
    u_short ip_id;          /* identification */
<br>
    u_short ip_off;         /* fragment offset field */
<br>
#define IP_RF 0x8000            /* reserved fragment flag */
<br>
#define IP_DF 0x4000            /* dont fragment flag */
<br>
#define IP_MF 0x2000            /* more fragments flag */
<br>
#define IP_OFFMASK 0x1fff       /* mask for fragmenting bits */
<br>
    u_int8_t ip_ttl;            /* time to live */
<br>
    u_int8_t ip_p;          /* protocol */
<br>
    u_short ip_sum;         /* checksum */
<br>
    struct in_addr ip_src, ip_dst;  /* source and dest address */
<br>
  };
<br>
 
<br>

<br>

<br>
其中ping程序只使用以下数据： 
<br>

<br>
IP报头长度IHL（Internet Header Length）――以４字节为一个单位来记录IP报头的长度，是上述IP数据结构的ip_hl变量。 
<br>
生存时间TTL（Time To Live）――以秒为单位，指出IP数据报能在网络上停留的最长时间，其值由发送方设定，并在经过路由的每一个节点时减一，当该值为０时，数据报将被丢弃，是上述IP数据结构的ip_ttl变量。
<br>

<br>
ICMP报头格式
<br>
ICMP报文分为两种，一是错误报告报文，二是查询报文。每个ICMP报头均包含类型、编码和校验和这三项内容，长度为８位，８位和１６位，其余选项则随ICMP的功能不同而不同。
<br>

<br>
Ping命令只使用众多ICMP报文中的两种："请求回送'(ICMP_ECHO)和"请求回应'(ICMP_ECHOREPLY)。在Linux中定义如下： 
<br>
#define ICMP_ECHO   0
<br>
#define ICMP_ECHOREPLY  8
<br>
 
<br>

<br>

<br>
这两种ICMP类型报头格式如下：
<br>

<br>

<br>

<br>
在Linux中ICMP数据结构(&lt;netinet/ip_icmp.h&gt;)定义如下： 
<br>
struct icmp
<br>
{
<br>
  u_int8_t  icmp_type;  /* type of message, see below */
<br>
  u_int8_t  icmp_code;  /* type sub code */
<br>
  u_int16_t icmp_cksum; /* ones complement checksum of struct */
<br>
  union
<br>
  {
<br>
    u_char ih_pptr;     /* ICMP_PARAMPROB */
<br>
    struct in_addr ih_gwaddr;   /* gateway address */
<br>
    struct ih_idseq     /* echo datagram */
<br>
    {
<br>
      u_int16_t icd_id;
<br>
      u_int16_t icd_seq;
<br>
    } ih_idseq;
<br>
    u_int32_t ih_void;
<br>

<br>
    /* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
<br>
    struct ih_pmtu
<br>
    {
<br>
      u_int16_t ipm_void;
<br>
      u_int16_t ipm_nextmtu;
<br>
    } ih_pmtu;
<br>

<br>
    struct ih_rtradv
<br>
    {
<br>
      u_int8_t irt_num_addrs;
<br>
      u_int8_t irt_wpa;
<br>
      u_int16_t irt_lifetime;
<br>
    } ih_rtradv;
<br>
  } icmp_hun;
<br>
#define icmp_pptr   icmp_hun.ih_pptr
<br>
#define icmp_gwaddr icmp_hun.ih_gwaddr
<br>
#define icmp_id     icmp_hun.ih_idseq.icd_id
<br>
#define icmp_seq        icmp_hun.ih_idseq.icd_seq
<br>
#define icmp_void   icmp_hun.ih_void
<br>
#define icmp_pmvoid icmp_hun.ih_pmtu.ipm_void
<br>
#define icmp_nextmtu    icmp_hun.ih_pmtu.ipm_nextmtu
<br>
#define icmp_num_addrs  icmp_hun.ih_rtradv.irt_num_addrs
<br>
#define icmp_wpa    icmp_hun.ih_rtradv.irt_wpa
<br>
#define icmp_lifetime   icmp_hun.ih_rtradv.irt_lifetime
<br>
  union
<br>
  {
<br>
    struct
<br>
    {
<br>
      u_int32_t its_otime;
<br>
      u_int32_t its_rtime;
<br>
      u_int32_t its_ttime;
<br>
    } id_ts;
<br>
    struct
<br>
    {
<br>
      struct ip idi_ip;
<br>
      /* options and then 64 bits of data */
<br>
    } id_ip;
<br>
    struct icmp_ra_addr id_radv;
<br>
    u_int32_t   id_mask;
<br>
    u_int8_t    id_data[1];
<br>
  } icmp_dun;
<br>
#define icmp_otime  icmp_dun.id_ts.its_otime
<br>
#define icmp_rtime  icmp_dun.id_ts.its_rtime
<br>
#define icmp_ttime  icmp_dun.id_ts.its_ttime
<br>
#define icmp_ip     icmp_dun.id_ip.idi_ip
<br>
#define icmp_radv   icmp_dun.id_radv
<br>
#define icmp_mask   icmp_dun.id_mask
<br>
#define icmp_data   icmp_dun.id_data
<br>
};
<br>

<br>
 
<br>

<br>

<br>
使用宏定义令表达更简洁,其中ICMP报头为８字节,数据报长度最大为64K字节。 
<br>

<br>
校验和算法――这一算法称为网际校验和算法，把被校验的数据１６位进行累加，然后取反码，若数据字节长度为奇数，则数据尾部补一个字节的０以凑成偶数。此
算法适用于IPv4、ICMPv4、IGMPV4、ICMPv6、UDP和TCP校验和，更详细的信息请参考RFC1071，校验和字段为上述ICMP数
据结构的icmp_cksum变量。 <br>
标识符――用于唯一标识ICMP报文, 为上述ICMP数据结构的icmp_id宏所指的变量。 
<br>
顺序号――ping命令的icmp_seq便由这里读出，代表ICMP报文的发送顺序，为上述ICMP数据结构的icmp_seq宏所指的变量。
<br>

<br>
ICMP数据报
<br>
Ping命令中需要显示的信息，包括icmp_seq和ttl都已有实现的办法，但还缺rtt往返时间。为了实现这一功能，可利用ICMP数据报携带一个时间戳。使用以下函数生成时间戳： 
<br>
#include 
<br>
int gettimeofday(struct timeval *tp,void *tzp)
<br>
其中timeval结构如下：
<br>
		struct timeval{
<br>
			long tv_sec;
<br>
			long tv_usec;
<br>
		}
<br>
 
<br>

<br>

<br>
其中tv_sec为秒数，tv_usec微秒数。在发送和接收报文时由gettimeofday分别生成两个timeval结构，两者之差即为往返时间,
即ICMP报文发送与接收的时间差，而timeval结构由ICMP数据报携带,tzp指针表示时区，一般都不使用，赋NULL值。
<br>

<br>
数据统计
<br>系统自带的ping命令当它接送完所有ICMP报文后，会对所有发送和所有接收的ICMP报文进行统计，从而计算ICMP报文丢失的比率。为达此
目的，定义两个全局变量：接收计数器和发送计数器，用于记录ICMP报文接受和发送数目。丢失数目=发送总数-接收总数，丢失比率=丢失数目/发送总数。
<br>

<br>
现给出模拟Ping程序功能的代码如下： 
<br>
/***********************************************************
<br>* 名称：myping.c                                          *
<br>
 * 说明:本程序用于演示ping命令的实现原理                   *
<br>
 ***********************************************************/
<br>
#include &lt;stdio.h&gt;
<br>
#include &lt;signal.h&gt;
<br>
#include &lt;arpa/inet.h&gt;
<br>
#include &lt;sys/types.h&gt;
<br>
#include &lt;sys/socket.h&gt;
<br>
#include &lt;unistd.h&gt;
<br>
#include &lt;netinet/in.h&gt;
<br>
#include &lt;netinet/ip.h&gt;
<br>
#include &lt;netinet/ip_icmp.h&gt;
<br>
#include &lt;netdb.h&gt;
<br>
#include &lt;setjmp.h&gt;
<br>
#include &lt;errno.h&gt;
<br>

<br>
#define PACKET_SIZE     4096
<br>
#define MAX_WAIT_TIME   5
<br>
#define MAX_NO_PACKETS  3
<br>

<br>
char sendpacket[PACKET_SIZE];
<br>
char recvpacket[PACKET_SIZE];
<br>
int sockfd,datalen=56;
<br>
int nsend=0,nreceived=0;
<br>
struct sockaddr_in dest_addr;
<br>
pid_t pid;
<br>
struct sockaddr_in from;
<br>
struct timeval tvrecv;
<br>

<br>
void statistics(int signo);
<br>
unsigned short cal_chksum(unsigned short *addr,int len);
<br>
int pack(int pack_no);
<br>
void send_packet(void);
<br>
void recv_packet(void);
<br>
int unpack(char *buf,int len);
<br>
void tv_sub(struct timeval *out,struct timeval *in);
<br>

<br>
void statistics(int signo)
<br>
{       printf("\n--------------------PING statistics-------------------\n");
<br>
        printf("%d packets transmitted, %d received , %%%d lost\n",nsend,nreceived,
<br>
                        (nsend-nreceived)/nsend*100);
<br>
        close(sockfd);
<br>
        exit(1);
<br>
}
<br>
/*校验和算法*/
<br>
unsigned short cal_chksum(unsigned short *addr,int len)
<br>
{       int nleft=len;
<br>
        int sum=0;
<br>
        unsigned short *w=addr;
<br>
        unsigned short answer=0;
<br>
		
<br>
/*把ICMP报头二进制数据以2字节为单位累加起来*/
<br>
        while(nleft&gt;1)
<br>
        {       sum+=*w++;
<br>
                nleft-=2;
<br>
        }
<br>
		/*若ICMP报头为奇数个字节，会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节，这个2字节数据的低字节为0，继续累加*/
<br>
        if( nleft==1)
<br>
        {       *(unsigned char *)(&amp;answer)=*(unsigned char *)w;
<br>
                sum+=answer;
<br>
        }
<br>
        sum=(sum&gt;&gt;16)+(sum&amp;0xffff);
<br>
        sum+=(sum&gt;&gt;16);
<br>
        answer=~sum;
<br>
        return answer;
<br>
}
<br>
/*设置ICMP报头*/
<br>
int pack(int pack_no)
<br>
{       int i,packsize;
<br>
        struct icmp *icmp;
<br>
        struct timeval *tval;
<br>

<br>
        icmp=(struct icmp*)sendpacket;
<br>
        icmp-&gt;icmp_type=ICMP_ECHO;
<br>
        icmp-&gt;icmp_code=0;
<br>
        icmp-&gt;icmp_cksum=0;
<br>
        icmp-&gt;icmp_seq=pack_no;
<br>
        icmp-&gt;icmp_id=pid;
<br>
        packsize=8+datalen;
<br>
        tval= (struct timeval *)icmp-&gt;icmp_data;
<br>
        gettimeofday(tval,NULL);    /*记录发送时间*/
<br>
        icmp-&gt;icmp_cksum=cal_chksum( (unsigned short *)icmp,packsize); /*校验算法*/
<br>
        return packsize;
<br>
}
<br>

<br>
/*发送三个ICMP报文*/
<br>
void send_packet()
<br>
{       int packetsize;
<br>
        while( nsend&lt;MAX_NO_PACKETS)
<br>
        {       nsend++;
<br>
                packetsize=pack(nsend); /*设置ICMP报头*/
<br>
                if( sendto(sockfd,sendpacket,packetsize,0,
<br>
                          (struct sockaddr *)&amp;dest_addr,sizeof(dest_addr) )&lt;0  )
<br>
                {       perror("sendto error");
<br>
                        continue;
<br>
                }
<br>
                sleep(1); /*每隔一秒发送一个ICMP报文*/
<br>
        }
<br>
}
<br>

<br>
/*接收所有ICMP报文*/
<br>
void recv_packet()
<br>
{       int n,fromlen;
<br>
        extern int errno;
<br>

<br>
        signal(SIGALRM,statistics);
<br>
        fromlen=sizeof(from);
<br>
        while( nreceived&lt;nsend)
<br>
        {       alarm(MAX_WAIT_TIME);
<br>
                if( (n=recvfrom(sockfd,recvpacket,sizeof(recvpacket),0,
<br>
                                (struct sockaddr *)&amp;from,&amp;fromlen)) &lt;0)
<br>
                {       if(errno==EINTR)continue;
<br>
                        perror("recvfrom error");
<br>
                        continue;
<br>
                }
<br>
                gettimeofday(&amp;tvrecv,NULL);  /*记录接收时间*/
<br>
                if(unpack(recvpacket,n)==-1)continue;
<br>
                nreceived++;
<br>
        }
<br>

<br>
}
<br>
/*剥去ICMP报头*/
<br>
int unpack(char *buf,int len)
<br>
{       int i,iphdrlen;
<br>
        struct ip *ip;
<br>
        struct icmp *icmp;
<br>
        struct timeval *tvsend;
<br>
        double rtt;
<br>

<br>
        ip=(struct ip *)buf;
<br>
        iphdrlen=ip-&gt;ip_hl&lt;&lt;2;    /*求ip报头长度,即ip报头的长度标志乘4*/
<br>
        icmp=(struct icmp *)(buf+iphdrlen);  /*越过ip报头,指向ICMP报头*/
<br>
        len-=iphdrlen;            /*ICMP报头及ICMP数据报的总长度*/
<br>
        if( len&lt;8)                /*小于ICMP报头长度则不合理*/
<br>
        {       printf("ICMP packets\'s length is less than 8\n");
<br>
                return -1;
<br>
        }
<br>
        /*确保所接收的是我所发的的ICMP的回应*/
<br>
        if( (icmp-&gt;icmp_type==ICMP_ECHOREPLY) &amp;&amp; (icmp-&gt;icmp_id==pid) )
<br>
        {       tvsend=(struct timeval *)icmp-&gt;icmp_data;
<br>
                tv_sub(&amp;tvrecv,tvsend);  /*接收和发送的时间差*/
<br>
                rtt=tvrecv.tv_sec*1000+tvrecv.tv_usec/1000;  /*以毫秒为单位计算rtt*/
<br>
                /*显示相关信息*/
<br>
                printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%.3f ms\n",
<br>
                        len,
<br>
                        inet_ntoa(from.sin_addr),
<br>
                        icmp-&gt;icmp_seq,
<br>
                        ip-&gt;ip_ttl,
<br>
                        rtt);
<br>
        }
<br>
        else    return -1;
<br>
}
<br>

<br>
main(int argc,char *argv[])
<br>
{       struct hostent *host;
<br>
        struct protoent *protocol;
<br>
        unsigned long inaddr=0l;
<br>
        int waittime=MAX_WAIT_TIME;
<br>
        int size=50*1024;
<br>

<br>
        if(argc&lt;2)
<br>
        {       printf("usage:%s hostname/IP address\n",argv[0]);
<br>
                exit(1);
<br>
        }
<br>

<br>
        if( (protocol=getprotobyname("icmp") )==NULL)
<br>
        {       perror("getprotobyname");
<br>
                exit(1);
<br>
        }
<br>
        /*生成使用ICMP的原始套接字,这种套接字只有root才能生成*/
<br>
        if( (sockfd=socket(AF_INET,SOCK_RAW,protocol-&gt;p_proto) )&lt;0)
<br>
        {       perror("socket error");
<br>
                exit(1);
<br>
        }
<br>
        /* 回收root权限,设置当前用户权限*/
<br>
        setuid(getuid());
<br>
        /*扩大套接字接收缓冲区到50K这样做主要为了减小接收缓冲区溢出的
<br>
          的可能性,若无意中ping一个广播地址或多播地址,将会引来大量应答*/
<br>
        setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&amp;size,sizeof(size) );
<br>
        bzero(&amp;dest_addr,sizeof(dest_addr));
<br>
        dest_addr.sin_family=AF_INET;
<br>

<br>
        /*判断是主机名还是ip地址*/
<br>
        if( inaddr=inet_addr(argv[1])==INADDR_NONE)
<br>
        {       if((host=gethostbyname(argv[1]) )==NULL) /*是主机名*/
<br>
                {       perror("gethostbyname error");
<br>
                        exit(1);
<br>
                }
<br>
                memcpy( (char *)&amp;dest_addr.sin_addr,host-&gt;h_addr,host-&gt;h_length);
<br>
        }
<br>
        else    /*是ip地址*/
<br>
                memcpy( (char *)&amp;dest_addr,(char *)&amp;inaddr,host-&gt;h_length);
<br>
        /*获取main的进程id,用于设置ICMP的标志符*/
<br>
        pid=getpid();
<br>
        printf("PING %s(%s): %d bytes data in ICMP packets.\n",argv[1],
<br>
                        inet_ntoa(dest_addr.sin_addr),datalen);
<br>
        send_packet();  /*发送所有ICMP报文*/
<br>
        recv_packet();  /*接收所有ICMP报文*/
<br>
        statistics(SIGALRM); /*进行统计*/
<br>

<br>
        return 0;
<br>

<br>
}
<br>
/*两个timeval结构相减*/
<br>
void tv_sub(struct timeval *out,struct timeval *in)
<br>
{       if( (out-&gt;tv_usec-=in-&gt;tv_usec)&lt;0)
<br>
        {       --out-&gt;tv_sec;
<br>
                out-&gt;tv_usec+=1000000;
<br>
        }
<br>
        out-&gt;tv_sec-=in-&gt;tv_sec;
<br>
}
<br>
/*------------- The End -----------*/
<br>

<br>
 
<br>

<br>

<br>
特别注意
<br>
只有root用户才能利用socket()函数生成原始套接字，要让Linux的一般用户能执行以上程序，需进行如下的特别操作：
<br>

<br>
用root登陆，编译以上程序：gcc -o myping myping.c，其目的有二：一是编译，二是让myping属于root用户。
<br>

<br>
再执行chmod u+s myping，目的是把myping程序设成SUID的属性。
<br>

<br>
退出root，用一般用户登陆，执行./myping <a href="http://www.cn.ibm.com%ef%bc%8c%e6%9c%89%e4%bb%a5%e4%b8%8b%e6%89%a7%e8%a1%8c%e7%bb%93%e6%9e%9c%ef%bc%9a/" target="_blank">www.cn.ibm.com，有以下执行结果：</a> 
<br>
PING <a href="http://www.cn.ibm.com%28202.95.2.148%29/" target="_blank">www.cn.ibm.com(202.95.2.148):</a> 56 bytes data in ICMP packets.
<br>
64 byte from 202.95.2.148: icmp_seq=1 ttl=242 rtt=3029.000 ms
<br>
64 byte from 202.95.2.148: icmp_seq=2 ttl=242 rtt=2020.000 ms
<br>
64 byte from 202.95.2.148: icmp_seq=3 ttl=242 rtt=1010.000 ms
<br>

<br>
--------------------PING statistics-------------------
<br>
3 packets transmitted, 3 received , %0 lost
<br>

<br>
 
<br>

<br>

<br>
由于myping.c是发送完所有的ICMP报文才去接收，因此第一、第二和第三个ICMP报文的往返时间依此是3秒，２秒，１秒，上述结果中rtt信息正反映这一事实。
<br>

</span><img src ="http://www.cnitblog.com/drizztzou/aggbug/3419.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-10-20 00:46 <a href="http://www.cnitblog.com/drizztzou/articles/3419.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>GCC 中文手册</title><link>http://www.cnitblog.com/drizztzou/articles/1881.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Sun, 14 Aug 2005 01:00:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/1881.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/1881.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/1881.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/1881.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/1881.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: GCC Section:&nbsp;GNU&nbsp;Tools&nbsp;(1) Updated:&nbsp;2003/12/05 Index&nbsp;Return&nbsp;to&nbsp;Main&nbsp;Contents&nbsp;&nbsp;&nbsp;&nbsp; NAME gcc,g++-GNU工程的C和C++编译器(egcs-1.1.2)&nbsp;&nbsp;&nbsp; 总...&nbsp;&nbsp;<a href='http://www.cnitblog.com/drizztzou/articles/1881.html'>阅读全文</a><img src ="http://www.cnitblog.com/drizztzou/aggbug/1881.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-08-14 09:00 <a href="http://www.cnitblog.com/drizztzou/articles/1881.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>用GDB调试程序</title><link>http://www.cnitblog.com/drizztzou/articles/1010.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Fri, 15 Jul 2005 11:54:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/1010.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/1010.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/1010.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/1010.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/1010.html</trackback:ping><description><![CDATA[GDB是一个强大的命令行调试工具。大家知道命令行的强大就是在于，其可以形成执行序列，形成脚本。UNIX下的软件全是命令行的，这给程序开发提代供了极大的便利，命令行软件的优势在于，它们可以非常容易的集成在一起，使用几个简单的已有工具的命令，就可以做出一个非常强大的功能。<BR><BR>于是UNIX下的软件比Windows下的软件更能有机地结合，各自发挥各自的长处，组合成更为强劲的功能。而Windows下的图形软件基本上是各自为营，互相不能调用，很不利于各种软件的相互集成。在这里并不是要和Windows做个什么比较，所谓“寸有所长，尺有所短”，图形化工具还是有不如命令行的地方。<BR><BR><B>用GDB调试程序</B><BR><BR>GDB概述<BR>————<BR><BR>GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。或许，各位比较喜欢那种图形界面方式的，像VC、BCB等IDE的调试，但如果你是在UNIX平台下做软件，你会发现GDB这个调试工具有比VC、BCB的图形化调试器更强大的功能。所谓“寸有所长，尺有所短”就是这个道理。<BR><BR>一般来说，GDB主要帮忙你完成下面四个方面的功能：<BR><BR>1、启动你的程序，可以按照你的自定义的要求随心所欲的运行程序。<BR>2、可让被调试的程序在你所指定的调置的断点处停住。（断点可以是条件表达式）<BR>3、当程序被停住时，可以检查此时你的程序中所发生的事。<BR>4、动态的改变你程序的执行环境。<BR><BR>从上面看来，GDB和一般的调试工具没有什么两样，基本上也是完成这些功能，不过在细节上，你会发现GDB这个调试工具的强大，大家可能比较习惯了图形化的调试工具，但有时候，命令行的调试工具却有着图形化工具所不能完成的功能。让我们一一看来。<BR><BR>一个调试示例<BR>——————<BR><BR>源程序：tst.c<BR><BR>1 #include <BR>2<BR>3 int func(int n)<BR>4 {<BR>5 int sum=0,i;<BR>6 for(i=0; i 7 {<BR>8 sum+=i;<BR>9 }<BR>10 return sum;<BR>11 }<BR>12<BR>13<BR>14 main()<BR>15 {<BR>16 int i;<BR>17 long result = 0;<BR>18 for(i=1; i&lt;=100; i++)<BR>19 {<BR>20 result += i;<BR>21 }<BR>22<BR>23 printf("result[1-100] = %d \n", result );<BR>24 printf("result[1-250] = %d \n", func(250) );<BR>25 }<BR><BR>编译生成执行文件：（Linux下）<BR>hchen/test&gt; cc -g tst.c -o tst<BR><BR>使用GDB调试：<BR><BR>hchen/test&gt; gdb tst &lt;---------- 启动GDB<BR>GNU gdb 5.1.1<BR>Copyright 2002 Free Software Foundation, Inc.<BR>GDB is free software, covered by the GNU General Public License, and you are<BR>welcome to change it and/or distribute copies of it under certain conditions.<BR>Type "show copying" to see the conditions.<BR>There is absolutely no warranty for GDB. Type "show warranty" for details.<BR>This GDB was configured as "i386-suse-linux"...<BR>(gdb) l &lt;-------------------- l命令相当于list，从第一行开始例出原码。<BR>1 #include <BR>2<BR>3 int func(int n)<BR>4 {<BR>5 int sum=0,i;<BR>6 for(i=0; i 7 {<BR>8 sum+=i;<BR>9 }<BR>10 return sum;<BR>(gdb) &lt;-------------------- 直接回车表示，重复上一次命令<BR>11 }<BR>12<BR>13<BR>14 main()<BR>15 {<BR>16 int i;<BR>17 long result = 0;<BR>18 for(i=1; i&lt;=100; i++)<BR>19 {<BR>20 result += i; <BR>(gdb) break 16 &lt;-------------------- 设置断点，在源程序第16行处。<BR>Breakpoint 1 at 0x8048496: file tst.c, line 16.<BR>(gdb) break func &lt;-------------------- 设置断点，在函数func()入口处。<BR>Breakpoint 2 at 0x8048456: file tst.c, line 5.<BR>(gdb) info break &lt;-------------------- 查看断点信息。<BR>Num Type Disp Enb Address What<BR>1 breakpoint keep y 0x08048496 in main at tst.c:16<BR>2 breakpoint keep y 0x08048456 in func at tst.c:5<BR>(gdb) r &lt;--------------------- 运行程序，run命令简写<BR>Starting program: /home/hchen/test/tst<BR><BR>Breakpoint 1, main () at tst.c:17 &lt;---------- 在断点处停住。<BR>17 long result = 0;<BR>(gdb) n &lt;--------------------- 单条语句执行，next命令简写。<BR>18 for(i=1; i&lt;=100; i++)<BR>(gdb) n<BR>20 result += i;<BR>(gdb) n<BR>18 for(i=1; i&lt;=100; i++)<BR>(gdb) n<BR>20 result += i;<BR>(gdb) c &lt;--------------------- 继续运行程序，continue命令简写。<BR>Continuing.<BR>result[1-100] = 5050 &lt;----------程序输出。<BR><BR>Breakpoint 2, func (n=250) at tst.c:5<BR>5 int sum=0,i;<BR>(gdb) n<BR>6 for(i=1; i&lt;=n; i++)<BR>(gdb) p i &lt;--------------------- 打印变量i的值，print命令简写。<BR>$1 = 134513808<BR>(gdb) n<BR>8 sum+=i;<BR>(gdb) n<BR>6 for(i=1; i&lt;=n; i++)<BR>(gdb) p sum<BR>$2 = 1<BR>(gdb) n<BR>8 sum+=i;<BR>(gdb) p i<BR>$3 = 2<BR>(gdb) n<BR>6 for(i=1; i&lt;=n; i++)<BR>(gdb) p sum<BR>$4 = 3<BR>(gdb) bt &lt;--------------------- 查看函数堆栈。<BR>#0 func (n=250) at tst.c:5<BR>#1 0x080484e4 in main () at tst.c:24<BR>#2 0x400409ed in __libc_start_main () from /lib/libc.so.6<BR>(gdb) finish &lt;--------------------- 退出函数。<BR>Run till exit from #0 func (n=250) at tst.c:5<BR>0x080484e4 in main () at tst.c:24<BR>24 printf("result[1-250] = %d \n", func(250) );<BR>Value returned is $6 = 31375<BR>(gdb) c &lt;--------------------- 继续运行。<BR>Continuing.<BR>result[1-250] = 31375 &lt;----------程序输出。<BR><BR>Program exited with code 027. &lt;--------程序退出，调试结束。<BR>(gdb) q &lt;--------------------- 退出gdb。<BR>hchen/test&gt;<BR><BR>好了，有了以上的感性认识，还是让我们来系统地认识一下gdb吧。<BR><BR>使用GDB<BR>————<BR><BR>一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序，首先在编译时，我们必须要把调试信息加到可执行文件中。使用编译器（cc/gcc/g++）的 -g 参数可以做到这一点。如：<BR><BR>&gt; cc -g hello.c -o hello<BR>&gt; g++ -g hello.cpp -o hello<BR><BR>如果没有-g，你将看不见程序的函数名、变量名，所代替的全是运行时的内存地址。当你用-g把调试信息加入之后，并成功编译目标代码以后，让我们来看看如何用gdb来调试他。<BR><BR>启动GDB的方法有以下几种：<BR><BR>1、gdb <BR>program也就是你的执行文件，一般在当然目录下。<BR><BR>2、gdb core<BR>用gdb同时调试一个运行程序和core文件，core是程序非法执行后core dump后产生的文件。<BR><BR>3、gdb <BR>如果你的程序是一个服务程序，那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去，并调试他。program应该在PATH环境变量中搜索得到。<BR><BR><BR>GDB启动时，可以加上一些GDB的启动开关，详细的开关可以用gdb -help查看。我在下面只例举一些比较常用的参数：<BR><BR>-symbols <BR>-s <BR>从指定文件中读取符号表。<BR><BR>-se file <BR>从指定文件中读取符号表信息，并把他用在可执行文件中。<BR><BR>-core <BR>-c <BR>调试时core dump的core文件。<BR><BR>-directory <BR>-d <BR>加入一个源文件的搜索路径。默认搜索路径是环境变量中PATH所定义的路径。<BR><BR>GDB的命令概貌<BR>———————<BR><BR>启动gdb后，就你被带入gdb的调试环境中，就可以使用gdb的命令开始调试程序了，gdb的命令可以使用help命令来查看，如下所示：<BR><BR>/home/hchen&gt; gdb<BR>GNU gdb 5.1.1<BR>Copyright 2002 Free Software Foundation, Inc.<BR>GDB is free software, covered by the GNU General Public License, and you are<BR>welcome to change it and/or distribute copies of it under certain conditions.<BR>Type "show copying" to see the conditions.<BR>There is absolutely no warranty for GDB. Type "show warranty" for details.<BR>This GDB was configured as "i386-suse-linux".<BR>(gdb) help<BR>List of classes of commands:<BR><BR>aliases -- Aliases of other commands<BR>breakpoints -- Making program stop at certain points<BR>data -- Examining data<BR>files -- Specifying and examining files<BR>internals -- Maintenance commands<BR>obscure -- Obscure features<BR>running -- Running the program<BR>stack -- Examining the stack<BR>status -- Status inquiries<BR>support -- Support facilities<BR>tracepoints -- Tracing of program execution without stopping the program<BR>user-defined -- User-defined commands<BR><BR>Type "help" followed by a class name for a list of commands in that class.<BR>Type "help" followed by command name for full documentation.<BR>Command name abbreviations are allowed if unambiguous.<BR>(gdb)<BR><BR>gdb的命令很多，gdb把之分成许多个种类。help命令只是例出gdb的命令种类，如果要看种类中的命令，可以使用help 命令，如：help breakpoints，查看设置断点的所有命令。也可以直接help 来查看命令的帮助。<BR><BR><BR>gdb中，输入命令时，可以不用打全命令，只用打命令的前几个字符就可以了，当然，命令的前几个字符应该要标志着一个唯一的命令，在Linux下，你可以敲击两次TAB键来补齐命令的全称，如果有重复的，那么gdb会把其例出来。<BR><BR>示例一：在进入函数func时，设置一个断点。可以敲入break func，或是直接就是b func<BR>(gdb) b func<BR>Breakpoint 1 at 0x8048458: file hello.c, line 10.<BR><BR>示例二：敲入b按两次TAB键，你会看到所有b打头的命令：<BR>(gdb) b<BR>backtrace break bt<BR>(gdb)<BR><BR>示例三：只记得函数的前缀，可以这样：<BR>(gdb) b make_ &lt;按TAB键&gt;<BR>（再按下一次TAB键，你会看到:）<BR>make_a_section_from_file make_environ<BR>make_abs_section make_function_type<BR>make_blockvector make_pointer_type<BR>make_cleanup make_reference_type<BR>make_command make_symbol_completion_list<BR>(gdb) b make_<BR>GDB把所有make开头的函数全部例出来给你查看。<BR><BR>示例四：调试C++的程序时，有可以函数名一样。如：<BR>(gdb) b 'bubble( M-? <BR>bubble(double,double) bubble(int,int)<BR>(gdb) b 'bubble(<BR>你可以查看到C++中的所有的重载函数及参数。（注：M-?和“按两次TAB键”是一个意思）<BR><BR>要退出gdb时，只用发quit或命令简称q就行了。<BR><BR>GDB中运行UNIX的shell程序<BR>————————————<BR><BR>在gdb环境中，你可以执行UNIX的shell的命令，使用gdb的shell命令来完成：<BR><BR>shell <BR>调用UNIX的shell来执行，环境变量SHELL中定义的UNIX的shell将会被用来执行，如果SHELL没有定义，那就使用UNIX的标准shell：/bin/sh。（在Windows中使用Command.com或cmd.exe）<BR><BR>还有一个gdb命令是make：<BR>make <BR>可以在gdb中执行make命令来重新build自己的程序。这个命令等价于“shell make ”。 <BR><BR>在GDB中运行程序<BR>————————<BR><BR>当以gdb 方式启动gdb后，gdb会在PATH路径和当前目录中搜索的源文件。如要确认gdb是否读到源文件，可使用l或list命令，看看gdb是否能列出源代码。<BR><BR>在gdb中，运行程序使用r或是run命令。程序的运行，你有可能需要设置下面四方面的事。<BR><BR>1、程序运行参数。<BR>set args 可指定运行时参数。（如：set args 10 20 30 40 50）<BR>show args 命令可以查看设置好的运行参数。<BR><BR>2、运行环境。<BR>path <BR><BR>可设定程序的运行路径。<BR>show paths 查看程序的运行路径。<BR>set environment varname [=value] 设置环境变量。如：set env USER=hchen<BR>show environment [varname] 查看环境变量。<BR><BR>3、工作目录。<BR>cd <BR><BR>相当于shell的cd命令。<BR>pwd 显示当前的所在目录。<BR><BR>4、程序的输入输出。<BR>info terminal 显示你程序用到的终端的模式。<BR>使用重定向控制程序输出。如：run &gt; outfile<BR>tty命令可以指写输入输出的终端设备。如：tty /dev/ttyb<BR><BR><BR>调试已运行的程序<BR>————————<BR><BR>两种方法：<BR>1、在UNIX下用ps查看正在运行的程序的PID（进程ID），然后用gdb PID格式挂接正在运行的程序。<BR>2、先用gdb 关联上源代码，并进行gdb，在gdb中用attach命令来挂接进程的PID。并用detach来取消挂接的进程。<BR><BR>暂停 / 恢复程序运行<BR>—————————<BR><BR>调试程序中，暂停程序运行是必须的，GDB可以方便地暂停程序的运行。你可以设置程序的在哪行停住，在什么条件下停住，在收到什么信号时停往等等。以便于你查看运行时的变量，以及运行时的流程。<BR><BR>当进程被gdb停住时，你可以使用info program 来查看程序的是否在运行，进程号，被暂停的原因。<BR><BR>在gdb中，我们可以有以下几种暂停方式：断点（BreakPoint）、观察点（WatchPoint）、捕捉点（CatchPoint）、信号（Signals）、线程停止（Thread Stops）。如果要恢复程序运行，可以使用c或是continue命令。<BR><BR>一、设置断点（BreakPoint）<BR><BR>我们用break命令来设置断点。正面有几点设置断点的方法：<BR><BR>break <BR>在进入指定函数时停住。C++中可以使用class::function或function(type,type)格式来指定函数名。<BR><BR>break <BR>在指定行号停住。<BR><BR>break +offset <BR>break -offset <BR>在当前行号的前面或后面的offset行停住。offiset为自然数。<BR><BR>break filename:linenum <BR>在源文件filename的linenum行处停住。<BR><BR>break filename:function <BR>在源文件filename的function函数的入口处停住。<BR><BR>break *address<BR>在程序运行的内存地址处停住。<BR><BR>break <BR>break命令没有参数时，表示在下一条指令处停住。<BR><BR>break ... if <BR>...可以是上述的参数，condition表示条件，在条件成立时停住。比如在循环境体中，可以设置break if i=100，表示当i为100时停住程序。<BR><BR>查看断点时，可使用info命令，如下所示：（注：n表示断点号）<BR>info breakpoints [n] <BR>info break [n] <BR><BR><BR>二、设置观察点（WatchPoint）<BR><BR>观察点一般来观察某个表达式（变量也是一种表达式）的值是否有变化了，如果有变化，马上停住程序。我们有下面的几种方法来设置观察点：<BR><BR>watch <BR>为表达式（变量）expr设置一个观察点。一量表达式值有变化时，马上停住程序。<BR><BR>rwatch <BR>当表达式（变量）expr被读时，停住程序。<BR><BR>awatch <BR>当表达式（变量）的值被读或被写时，停住程序。<BR><BR>info watchpoints<BR>列出当前所设置了的所有观察点。<BR><BR>三、设置捕捉点（CatchPoint）<BR><BR>你可设置捕捉点来补捉程序运行时的一些事件。如：载入共享库（动态链接库）或是C++的异常。设置捕捉点的格式为：<BR><BR>catch <BR>当event发生时，停住程序。event可以是下面的内容：<BR>1、throw 一个C++抛出的异常。（throw为关键字）<BR>2、catch 一个C++捕捉到的异常。（catch为关键字）<BR>3、exec 调用系统调用exec时。（exec为关键字，目前此功能只在HP-UX下有用）<BR>4、fork 调用系统调用fork时。（fork为关键字，目前此功能只在HP-UX下有用）<BR>5、vfork 调用系统调用vfork时。（vfork为关键字，目前此功能只在HP-UX下有用）<BR>6、load 或 load 载入共享库（动态链接库）时。（load为关键字，目前此功能只在HP-UX下有用）<BR>7、unload 或 unload 卸载共享库（动态链接库）时。（unload为关键字，目前此功能只在HP-UX下有用）<BR><BR>tcatch <BR>只设置一次捕捉点，当程序停住以后，应点被自动删除。<BR><BR>四、维护停止点<BR><BR>上面说了如何设置程序的停止点，GDB中的停止点也就是上述的三类。在GDB中，如果你觉得已定义好的停止点没有用了，你可以使用delete、clear、disable、enable这几个命令来进行维护。<BR><BR>clear<BR>清除所有的已定义的停止点。<BR><BR>clear <BR>clear <BR>清除所有设置在函数上的停止点。<BR><BR>clear <BR>clear <BR>清除所有设置在指定行上的停止点。<BR><BR>delete [breakpoints] [range...]<BR>删除指定的断点，breakpoints为断点号。如果不指定断点号，则表示删除所有的断点。range 表示断点号的范围（如：3-7）。其简写命令为d。<BR><BR>比删除更好的一种方法是disable停止点，disable了的停止点，GDB不会删除，当你还需要时，enable即可，就好像回收站一样。<BR><BR>disable [breakpoints] [range...]<BR>disable所指定的停止点，breakpoints为停止点号。如果什么都不指定，表示disable所有的停止点。简写命令是dis.<BR><BR>enable [breakpoints] [range...]<BR>enable所指定的停止点，breakpoints为停止点号。<BR><BR>enable [breakpoints] once range...<BR>enable所指定的停止点一次，当程序停止后，该停止点马上被GDB自动disable。<BR><BR>enable [breakpoints] delete range...<BR>enable所指定的停止点一次，当程序停止后，该停止点马上被GDB自动删除。<BR><BR>五、停止条件维护<BR><BR>前面在说到设置断点时，我们提到过可以设置一个条件，当条件成立时，程序自动停止，这是一个非常强大的功能，这里，我想专门说说这个条件的相关维护命令。一般来说，为断点设置一个条件，我们使用if关键词，后面跟其断点条件。并且，条件设置好后，我们可以用condition命令来修改断点的条件。（只有break和watch命令支持if，catch目前暂不支持if）<BR><BR>condition <BR>修改断点号为bnum的停止条件为expression。<BR><BR>condition <BR>清除断点号为bnum的停止条件。<BR><BR><BR>还有一个比较特殊的维护命令ignore，你可以指定程序运行时，忽略停止条件几次。<BR><BR>ignore <BR>表示忽略断点号为bnum的停止条件count次。<BR><BR>六、为停止点设定运行命令<BR><BR>我们可以使用GDB提供的command命令来设置停止点的运行命令。也就是说，当运行的程序在被停止住时，我们可以让其自动运行一些别的命令，这很有利行自动化调试。对基于GDB的自动化调试是一个强大的支持。<BR><BR><BR>commands [bnum]<BR>... command-list ...<BR>end<BR><BR>为断点号bnum指写一个命令列表。当程序被该断点停住时，gdb会依次运行命令列表中的命令。<BR><BR>例如：<BR><BR>break foo if x&gt;0<BR>commands<BR>printf "x is %d\n",x<BR>continue<BR>end<BR>断点设置在函数foo中，断点条件是x&gt;0，如果程序被断住后，也就是，一旦x的值在foo函数中大于0，GDB会自动打印出x的值，并继续运行程序。<BR><BR>如果你要清除断点上的命令序列，那么只要简单的执行一下commands命令，并直接在打个end就行了。<BR><BR>七、断点菜单<BR><BR>在C++中，可能会重复出现同一个名字的函数若干次（函数重载），在这种情况下，break 不能告诉GDB要停在哪个函数的入口。当然，你可以使用break 也就是把函数的参数类型告诉GDB，以指定一个函数。否则的话，GDB会给你列出一个断点菜单供你选择你所需要的断点。你只要输入你菜单列表中的编号就可以了。如：<BR><BR>(gdb) b String::after<BR>[0] cancel<BR>[1] all<BR>[2] file:String.cc; line number:867<BR>[3] file:String.cc; line number:860<BR>[4] file:String.cc; line number:875<BR>[5] file:String.cc; line number:853<BR>[6] file:String.cc; line number:846<BR>[7] file:String.cc; line number:735<BR>&gt; 2 4 6<BR>Breakpoint 1 at 0xb26c: file String.cc, line 867.<BR>Breakpoint 2 at 0xb344: file String.cc, line 875.<BR>Breakpoint 3 at 0xafcc: file String.cc, line 846.<BR>Multiple breakpoints were set.<BR>Use the "delete" command to delete unwanted<BR>breakpoints.<BR>(gdb)<BR><BR>可见，GDB列出了所有after的重载函数，你可以选一下列表编号就行了。0表示放弃设置断点，1表示所有函数都设置断点。<BR><BR>八、恢复程序运行和单步调试<BR><BR>当程序被停住了，你可以用continue命令恢复程序的运行直到程序结束，或下一个断点到来。也可以使用step或next命令单步跟踪程序。<BR><BR>continue [ignore-count]<BR>c [ignore-count]<BR>fg [ignore-count]<BR>恢复程序运行，直到程序结束，或是下一个断点到来。ignore-count表示忽略其后的断点次数。continue，c，fg三个命令都是一样的意思。<BR><BR><BR>step <BR>单步跟踪，如果有函数调用，他会进入该函数。进入函数的前提是，此函数被编译有debug信息。很像VC等工具中的step in。后面可以加count也可以不加，不加表示一条条地执行，加表示执行后面的count条指令，然后再停住。<BR><BR>next <BR>同样单步跟踪，如果有函数调用，他不会进入该函数。很像VC等工具中的step over。后面可以加count也可以不加，不加表示一条条地执行，加表示执行后面的count条指令，然后再停住。<BR><BR>set step-mode<BR>set step-mode on<BR>打开step-mode模式，于是，在进行单步跟踪时，程序不会因为没有debug信息而不停住。这个参数有很利于查看机器码。<BR><BR>set step-mod off<BR>关闭step-mode模式。<BR><BR>finish<BR>运行程序，直到当前函数完成返回。并打印函数返回时的堆栈地址和返回值及参数值等信息。<BR><BR>until 或 u<BR>当你厌倦了在一个循环体内单步跟踪时，这个命令可以运行程序直到退出循环体。<BR><BR>stepi 或 si<BR>nexti 或 ni<BR>单步跟踪一条机器指令！一条程序代码有可能由数条机器指令完成，stepi和nexti可以单步执行机器指令。与之一样有相同功能的命令是“display/i $pc” ，当运行完这个命令后，单步跟踪会在打出程序代码的同时打出机器指令（也就是汇编代码）<BR><BR>九、信号（Signals）<BR><BR>信号是一种软中断，是一种处理异步事件的方法。一般来说，操作系统都支持许多信号。尤其是UNIX，比较重要应用程序一般都会处理信号。UNIX定义了许多信号，比如SIGINT表示中断字符信号，也就是Ctrl+C的信号，SIGBUS表示硬件故障的信号；SIGCHLD表示子进程状态改变信号；SIGKILL表示终止程序运行的信号，等等。信号量编程是UNIX下非常重要的一种技术。<BR><BR>GDB有能力在你调试程序的时候处理任何一种信号，你可以告诉GDB需要处理哪一种信号。你可以要求GDB收到你所指定的信号时，马上停住正在运行的程序，以供你进行调试。你可以用GDB的handle命令来完成这一功能。<BR><BR>handle <BR>在GDB中定义一个信号处理。信号可以以SIG开头或不以SIG开头，可以用定义一个要处理信号的范围（如：SIGIO-SIGKILL，表示处理从SIGIO信号到SIGKILL的信号，其中包括SIGIO，SIGIOT，SIGKILL三个信号），也可以使用关键字all来标明要处理所有的信号。一旦被调试的程序接收到信号，运行程序马上会被GDB停住，以供调试。其可以是以下几种关键字的一个或多个。<BR><BR>nostop<BR>当被调试的程序收到信号时，GDB不会停住程序的运行，但会打出消息告诉你收到这种信号。<BR>stop<BR>当被调试的程序收到信号时，GDB会停住你的程序。<BR>print<BR>当被调试的程序收到信号时，GDB会显示出一条信息。<BR>noprint<BR>当被调试的程序收到信号时，GDB不会告诉你收到信号的信息。<BR>pass<BR>noignore<BR>当被调试的程序收到信号时，GDB不处理信号。这表示，GDB会把这个信号交给被调试程序会处理。<BR>nopass<BR>ignore<BR>当被调试的程序收到信号时，GDB不会让被调试程序来处理这个信号。<BR><BR><BR>info signals<BR>info handle<BR>查看有哪些信号在被GDB检测中。<BR><BR>十、线程（Thread Stops）<BR><BR>如果你程序是多线程的话，你可以定义你的断点是否在所有的线程上，或是在某个特定的线程。GDB很容易帮你完成这一工作。<BR><BR>break thread <BR>break thread if ...<BR>linespec指定了断点设置在的源程序的行号。threadno指定了线程的ID，注意，这个ID是GDB分配的，你可以通过“info threads”命令来查看正在运行程序中的线程信息。如果你不指定thread 则表示你的断点设在所有线程上面。你还可以为某线程指定断点条件。如：<BR><BR>(gdb) break frik.c:13 thread 28 if bartab &gt; lim<BR><BR>当你的程序被GDB停住时，所有的运行线程都会被停住。这方便你你查看运行程序的总体情况。而在你恢复程序运行时，所有的线程也会被恢复运行。那怕是主进程在被单步调试时。<BR><BR>查看栈信息<BR>—————<BR><BR>当程序被停住了，你需要做的第一件事就是查看程序是在哪里停住的。当你的程序调用了一个函数，函数的地址，函数参数，函数内的局部变量都会被压入“栈”（Stack）中。你可以用GDB命令来查看当前的栈中的信息。<BR><BR>下面是一些查看函数调用栈信息的GDB命令：<BR><BR>backtrace <BR>bt <BR>打印当前的函数调用栈的所有信息。如：<BR><BR>(gdb) bt<BR>#0 func (n=250) at tst.c:6<BR>#1 0x08048524 in main (argc=1, argv=0xbffff674) at tst.c:30<BR>#2 0x400409ed in __libc_start_main () from /lib/libc.so.6<BR><BR>从上可以看出函数的调用栈信息：__libc_start_main --&gt; main() --&gt; func()<BR><BR><BR>backtrace <BR>bt <BR>n是一个正整数，表示只打印栈顶上n层的栈信息。<BR><BR>backtrace &lt;-n&gt; <BR>bt &lt;-n&gt; <BR>-n表一个负整数，表示只打印栈底下n层的栈信息。<BR><BR>如果你要查看某一层的信息，你需要在切换当前的栈，一般来说，程序停止时，最顶层的栈就是当前栈，如果你要查看栈下面层的详细信息，首先要做的是切换当前栈。<BR><BR>frame <BR>f <BR>n是一个从0开始的整数，是栈中的层编号。比如：frame 0，表示栈顶，frame 1，表示栈的第二层。<BR><BR>up <BR>表示向栈的上面移动n层，可以不打n，表示向上移动一层。 <BR><BR>down <BR>表示向栈的下面移动n层，可以不打n，表示向下移动一层。 <BR><BR><BR>上面的命令，都会打印出移动到的栈层的信息。如果你不想让其打出信息。你可以使用这三个命令：<BR><BR>select-frame 对应于 frame 命令。<BR>up-silently 对应于 up 命令。<BR>down-silently 对应于 down 命令。<BR><BR><BR>查看当前栈层的信息，你可以用以下GDB命令：<BR><BR>frame 或 f <BR>会打印出这些信息：栈的层编号，当前的函数名，函数参数值，函数所在文件及行号，函数执行到的语句。<BR><BR>info frame <BR>info f <BR>这个命令会打印出更为详细的当前栈层的信息，只不过，大多数都是运行时的内内地址。比如：函数地址，调用函数的地址，被调用函数的地址，目前的函数是由什么样的程序语言写成的、函数参数地址及值、局部变量的地址等等。如：<BR>(gdb) info f<BR>Stack level 0, frame at 0xbffff5d4:<BR>eip = 0x804845d in func (tst.c:6); saved eip 0x8048524<BR>called by frame at 0xbffff60c<BR>source language c.<BR>Arglist at 0xbffff5d4, args: n=250<BR>Locals at 0xbffff5d4, Previous frame's sp is 0x0<BR>Saved registers:<BR>ebp at 0xbffff5d4, eip at 0xbffff5d8<BR><BR>info args<BR>打印出当前函数的参数名及其值。<BR><BR>info locals<BR>打印出当前函数中所有局部变量及其值。<BR><BR>info catch<BR>打印出当前的函数中的异常处理信息。<BR><BR><BR>查看源程序<BR>—————<BR><BR>一、显示源代码<BR><BR>GDB 可以打印出所调试程序的源代码，当然，在程序编译时一定要加上-g的参数，把源程序信息编译到执行文件中。不然就看不到源程序了。当程序停下来以后，GDB会报告程序停在了那个文件的第几行上。你可以用list命令来打印程序的源代码。还是来看一看查看源代码的GDB命令吧。<BR><BR>list <BR>显示程序第linenum行的周围的源程序。<BR><BR>list <BR>显示函数名为function的函数的源程序。<BR><BR>list <BR>显示当前行后面的源程序。<BR><BR>list - <BR>显示当前行前面的源程序。<BR><BR>一般是打印当前行的上5行和下5行，如果显示函数是是上2行下8行，默认是10行，当然，你也可以定制显示的范围，使用下面命令可以设置一次显示源程序的行数。<BR><BR>set listsize <BR>设置一次显示源代码的行数。<BR><BR>show listsize<BR>查看当前listsize的设置。<BR><BR><BR>list命令还有下面的用法：<BR><BR>list , <BR>显示从first行到last行之间的源代码。<BR><BR>list , <BR>显示从当前行到last行之间的源代码。<BR><BR>list +<BR>往后显示源代码。<BR><BR><BR>一般来说在list后面可以跟以下这们的参数：<BR><BR>行号。<BR>&lt;+offset&gt; 当前行号的正偏移量。<BR>&lt;-offset&gt; 当前行号的负偏移量。<BR>哪个文件的哪一行。<BR>函数名。<BR>哪个文件中的哪个函数。<BR>&lt;*address&gt; 程序运行时的语句在内存中的地址。<BR><BR><BR>二、搜索源代码<BR><BR>不仅如此，GDB还提供了源代码搜索的命令：<BR><BR>forward-search <BR>search <BR>向前面搜索。<BR><BR>reverse-search <BR>全部搜索。<BR><BR>其中，就是正则表达式，也主一个字符串的匹配模式，关于正则表达式，我就不在这里讲了，还请各位查看相关资料。<BR><BR><BR>三、指定源文件的路径<BR><BR>某些时候，用-g编译过后的执行程序中只是包括了源文件的名字，没有路径名。GDB提供了可以让你指定源文件的路径的命令，以便GDB进行搜索。<BR><BR>directory <BR>dir <BR>加一个源文件路径到当前路径的前面。如果你要指定多个路径，UNIX下你可以使用“:”，Windows下你可以使用“;”。<BR>directory <BR>清除所有的自定义的源文件搜索路径信息。<BR><BR>show directories <BR>显示定义了的源文件搜索路径。<BR><BR><BR>四、源代码的内存<BR><BR>你可以使用info line命令来查看源代码在内存中的地址。info line后面可以跟“行号”，“函数名”，“文件名:行号”，“文件名:函数名”，这个命令会打印出所指定的源码在运行时的内存地址，如：<BR><BR>(gdb) info line tst.c:func<BR>Line 5 of "tst.c" starts at address 0x8048456 and ends at 0x804845d .<BR><BR>还有一个命令（disassemble）你可以查看源程序的当前执行时的机器码，这个命令会把目前内存中的指令dump出来。如下面的示例表示查看函数func的汇编代码。<BR><BR>(gdb) disassemble func<BR>Dump of assembler code for function func:<BR>0x8048450 : push %ebp<BR>0x8048451 : mov %esp,%ebp<BR>0x8048453 : sub $0x18,%esp<BR>0x8048456 : movl $0x0,0xfffffffc(%ebp)<BR>0x804845d : movl $0x1,0xfffffff8(%ebp)<BR>0x8048464 : mov 0xfffffff8(%ebp),%eax<BR>0x8048467 : cmp 0x8(%ebp),%eax<BR>0x804846a : jle 0x8048470 <BR>0x804846c : jmp 0x8048480 <BR>0x804846e : mov %esi,%esi<BR>0x8048470 : mov 0xfffffff8(%ebp),%eax<BR>0x8048473 : add %eax,0xfffffffc(%ebp)<BR>0x8048476 : incl 0xfffffff8(%ebp)<BR>0x8048479 : jmp 0x8048464 <BR>0x804847b : nop<BR>0x804847c : lea 0x0(%esi,1),%esi<BR>0x8048480 : mov 0xfffffffc(%ebp),%edx<BR>0x8048483 : mov %edx,%eax<BR>0x8048485 : jmp 0x8048487 <BR>0x8048487 : mov %ebp,%esp<BR>0x8048489 : pop %ebp<BR>0x804848a : ret<BR>End of assembler dump.<BR><BR><BR>查看运行时数据<BR>———————<BR><BR>在你调试程序时，当程序被停住时，你可以使用print命令（简写命令为p），或是同义命令inspect来查看当前程序的运行数据。print命令的格式是：<BR><BR>print <BR>print / <BR>是表达式，是你所调试的程序的语言的表达式（GDB可以调试多种编程语言），是输出的格式，比如，如果要把表达式按16进制的格式输出，那么就是/x。<BR><BR><BR>一、表达式<BR><BR>print和许多GDB的命令一样，可以接受一个表达式，GDB会根据当前的程序运行的数据来计算这个表达式，既然是表达式，那么就可以是当前程序运行中的const常量、变量、函数等内容。可惜的是GDB不能使用你在程序中所定义的宏。<BR><BR>表达式的语法应该是当前所调试的语言的语法，由于C/C++是一种大众型的语言，所以，本文中的例子都是关于C/C++的。（而关于用GDB调试其它语言的章节，我将在后面介绍）<BR><BR>在表达式中，有几种GDB所支持的操作符，它们可以用在任何一种语言中。<BR><BR>@<BR>是一个和数组有关的操作符，在后面会有更详细的说明。<BR><BR>::<BR>指定一个在文件或是一个函数中的变量。<BR><BR>{} <BR>表示一个指向内存地址的类型为type的一个对象。<BR><BR><BR>二、程序变量<BR><BR>在GDB中，你可以随时查看以下三种变量的值：<BR>1、全局变量（所有文件可见的）<BR>2、静态全局变量（当前文件可见的）<BR>3、局部变量（当前Scope可见的）<BR><BR>如果你的局部变量和全局变量发生冲突（也就是重名），一般情况下是局部变量会隐藏全局变量，也就是说，如果一个全局变量和一个函数中的局部变量同名时，如果当前停止点在函数中，用print显示出的变量的值会是函数中的局部变量的值。如果此时你想查看全局变量的值时，你可以使用“::”操作符：<BR><BR>file::variable<BR>function::variable<BR>可以通过这种形式指定你所想查看的变量，是哪个文件中的或是哪个函数中的。例如，查看文件f2.c中的全局变量x的值：<BR><BR>gdb) p 'f2.c'::x<BR><BR>当然，“::”操作符会和C++中的发生冲突，GDB能自动识别“::” 是否C++的操作符，所以你不必担心在调试C++程序时会出现异常。<BR><BR>另外，需要注意的是，如果你的程序编译时开启了优化选项，那么在用GDB调试被优化过的程序时，可能会发生某些变量不能访问，或是取值错误码的情况。这个是很正常的，因为优化程序会删改你的程序，整理你程序的语句顺序，剔除一些无意义的变量等，所以在GDB调试这种程序时，运行时的指令和你所编写指令就有不一样，也就会出现你所想象不到的结果。对付这种情况时，需要在编译程序时关闭编译优化。一般来说，几乎所有的编译器都支持编译优化的开关，例如，GNU的C/C++编译器GCC，你可以使用“-gstabs”选项来解决这个问题。关于编译器的参数，还请查看编译器的使用说明文档。<BR><BR><BR>三、数组<BR><BR>有时候，你需要查看一段连续的内存空间的值。比如数组的一段，或是动态分配的数据的大小。你可以使用GDB的“@”操作符，“@”的左边是第一个内存的地址的值，“@”的右边则你你想查看内存的长度。例如，你的程序中有这样的语句：<BR><BR>int *array = (int *) malloc (len * sizeof (int));<BR><BR>于是，在GDB调试过程中，你可以以如下命令显示出这个动态数组的取值：<BR><BR>p *array@len<BR><BR>@的左边是数组的首地址的值，也就是变量array所指向的内容，右边则是数据的长度，其保存在变量len中，其输出结果，大约是下面这个样子的：<BR><BR>(gdb) p *array@len<BR>$1 = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40}<BR><BR>如果是静态数组的话，可以直接用print数组名，就可以显示数组中所有数据的内容了。<BR><BR><BR>四、输出格式<BR><BR>一般来说，GDB会根据变量的类型输出变量的值。但你也可以自定义GDB的输出的格式。例如，你想输出一个整数的十六进制，或是二进制来查看这个整型变量的中的位的情况。要做到这样，你可以使用GDB的数据显示格式：<BR><BR>x 按十六进制格式显示变量。<BR>d 按十进制格式显示变量。<BR>u 按十六进制格式显示无符号整型。<BR>o 按八进制格式显示变量。<BR>t 按二进制格式显示变量。 <BR>a 按十六进制格式显示变量。<BR>c 按字符格式显示变量。<BR>f 按浮点数格式显示变量。<BR><BR>(gdb) p i<BR>$21 = 101 <BR><BR>(gdb) p/a i<BR>$22 = 0x65<BR><BR>(gdb) p/c i<BR>$23 = 101 'e'<BR><BR>(gdb) p/f i<BR>$24 = 1.41531145e-43<BR><BR>(gdb) p/x i<BR>$25 = 0x65<BR><BR>(gdb) p/t i<BR>$26 = 1100101<BR><BR><BR>五、查看内存<BR><BR>你可以使用examine命令（简写是x）来查看内存地址中的值。x命令的语法如下所示：<BR><BR>x/ <BR><BR>n、f、u是可选的参数。<BR><BR>n 是一个正整数，表示显示内存的长度，也就是说从当前地址向后显示几个地址的内容。<BR>f 表示显示的格式，参见上面。如果地址所指的是字符串，那么格式可以是s，如果地十是指令地址，那么格式可以是i。<BR>u 表示从当前地址往后请求的字节数，如果不指定的话，GDB默认是4个bytes。u参数可以用下面的字符来代替，b表示单字节，h表示双字节，w表示四字节，g表示八字节。当我们指定了字节长度后，GDB会从指内存定的内存地址开始，读写指定字节，并把其当作一个值取出来。<BR><BR>表示一个内存地址。<BR><BR>n/f/u三个参数可以一起使用。例如：<BR><BR>命令：x/3uh 0x54320 表示，从内存地址0x54320读取内容，h表示以双字节为一个单位，3表示三个单位，u表示按十六进制显示。<BR><BR><BR>六、自动显示<BR><BR>你可以设置一些自动显示的变量，当程序停住时，或是在你单步跟踪时，这些变量会自动显示。相关的GDB命令是display。<BR><BR>display <BR>display/ <BR>display/ <BR><BR>expr是一个表达式，fmt表示显示的格式，addr表示内存地址，当你用display设定好了一个或多个表达式后，只要你的程序被停下来，GDB会自动显示你所设置的这些表达式的值。<BR><BR>格式i和s同样被display支持，一个非常有用的命令是：<BR><BR>display/i $pc<BR><BR>$pc是GDB的环境变量，表示着指令的地址，/i则表示输出格式为机器指令码，也就是汇编。于是当程序停下后，就会出现源代码和机器指令码相对应的情形，这是一个很有意思的功能。<BR><BR>下面是一些和display相关的GDB命令：<BR><BR>undisplay <BR>delete display <BR>删除自动显示，dnums意为所设置好了的自动显式的编号。如果要同时删除几个，编号可以用空格分隔，如果要删除一个范围内的编号，可以用减号表示（如：2-5）<BR><BR>disable display <BR>enable display <BR>disable和enalbe不删除自动显示的设置，而只是让其失效和恢复。<BR><BR>info display<BR>查看display设置的自动显示的信息。GDB会打出一张表格，向你报告当然调试中设置了多少个自动显示设置，其中包括，设置的编号，表达式，是否enable。<BR><BR><BR>七、设置显示选项<BR><BR>GDB中关于显示的选项比较多，这里我只例举大多数常用的选项。<BR><BR>set print address <BR>set print address on <BR>打开地址输出，当程序显示函数信息时，GDB会显出函数的参数地址。系统默认为打开的，如：<BR><BR>(gdb) f<BR>#0 set_quotes (lq=0x34c78 "&lt;&lt;", rq=0x34c88 "&gt;&gt;")<BR>at input.c:530<BR>530 if (lquote != def_lquote)<BR><BR><BR>set print address off <BR>关闭函数的参数地址显示，如：<BR><BR>(gdb) set print addr off<BR>(gdb) f<BR>#0 set_quotes (lq="&lt;&lt;", rq="&gt;&gt;") at input.c:530<BR>530 if (lquote != def_lquote)<BR><BR>show print address <BR>查看当前地址显示选项是否打开。<BR><BR>set print array <BR>set print array on <BR>打开数组显示，打开后当数组显示时，每个元素占一行，如果不打开的话，每个元素则以逗号分隔。这个选项默认是关闭的。与之相关的两个命令如下，我就不再多说了。<BR><BR>set print array off <BR>show print array <BR><BR>set print elements <BR>这个选项主要是设置数组的，如果你的数组太大了，那么就可以指定一个来指定数据显示的最大长度，当到达这个长度时，GDB就不再往下显示了。如果设置为0，则表示不限制。<BR><BR>show print elements <BR>查看print elements的选项信息。<BR><BR>set print null-stop <BR>如果打开了这个选项，那么当显示字符串时，遇到结束符则停止显示。这个选项默认为off。<BR><BR>set print pretty on <BR>如果打开printf pretty这个选项，那么当GDB显示结构体时会比较漂亮。如：<BR><BR>$1 = {<BR>next = 0x0,<BR>flags = {<BR>sweet = 1,<BR>sour = 1<BR>},<BR>meat = 0x54 "Pork"<BR>}<BR><BR>set print pretty off<BR>关闭printf pretty这个选项，GDB显示结构体时会如下显示：<BR><BR>$1 = {next = 0x0, flags = {sweet = 1, sour = 1}, meat = 0x54 "Pork"}<BR><BR>show print pretty <BR>查看GDB是如何显示结构体的。<BR><BR><BR>set print sevenbit-strings <BR>设置字符显示，是否按“\nnn”的格式显示，如果打开，则字符串或字符数据按\nnn显示，如“\065”。<BR><BR>show print sevenbit-strings<BR>查看字符显示开关是否打开。 <BR><BR>set print union <BR>设置显示结构体时，是否显式其内的联合体数据。例如有以下数据结构：<BR><BR>typedef enum {Tree, Bug} Species;<BR>typedef enum {Big_tree, Acorn, Seedling} Tree_forms;<BR>typedef enum {Caterpillar, Cocoon, Butterfly}<BR>Bug_forms;<BR><BR>struct thing {<BR>Species it;<BR>union {<BR>Tree_forms tree;<BR>Bug_forms bug;<BR>} form;<BR>};<BR><BR>struct thing foo = {Tree, {Acorn}};<BR><BR>当打开这个开关时，执行 p foo 命令后，会如下显示：<BR>$1 = {it = Tree, form = {tree = Acorn, bug = Cocoon}}<BR><BR>当关闭这个开关时，执行 p foo 命令后，会如下显示：<BR>$1 = {it = Tree, form = {...}}<BR><BR>show print union<BR>查看联合体数据的显示方式<BR><BR>set print object <BR>在C++中，如果一个对象指针指向其派生类，如果打开这个选项，GDB会自动按照虚方法调用的规则显示输出，如果关闭这个选项的话，GDB就不管虚函数表了。这个选项默认是off。<BR><BR>show print object<BR>查看对象选项的设置。<BR><BR>set print static-members <BR>这个选项表示，当显示一个C++对象中的内容是，是否显示其中的静态数据成员。默认是on。<BR><BR>show print static-members<BR>查看静态数据成员选项设置。<BR><BR>set print vtbl <BR>当此选项打开时，GDB将用比较规整的格式来显示虚函数表时。其默认是关闭的。<BR><BR>show print vtbl<BR>查看虚函数显示格式的选项。<BR><BR><BR>八、历史记录<BR><BR>当你用GDB的print查看程序运行时的数据时，你每一个print都会被GDB记录下来。GDB会以$1, $2, $3 .....这样的方式为你每一个print命令编上号。于是，你可以使用这个编号访问以前的表达式，如$1。这个功能所带来的好处是，如果你先前输入了一个比较长的表达式，如果你还想查看这个表达式的值，你可以使用历史记录来访问，省去了重复输入。<BR><BR><BR>九、GDB环境变量<BR><BR>你可以在GDB的调试环境中定义自己的变量，用来保存一些调试程序中的运行数据。要定义一个GDB的变量很简单只需。使用GDB的set命令。GDB的环境变量和UNIX一样，也是以$起头。如：<BR><BR>set $foo = *object_ptr<BR><BR>使用环境变量时，GDB会在你第一次使用时创建这个变量，而在以后的使用中，则直接对其賦值。环境变量没有类型，你可以给环境变量定义任一的类型。包括结构体和数组。<BR><BR>show convenience <BR>该命令查看当前所设置的所有的环境变量。<BR><BR>这是一个比较强大的功能，环境变量和程序变量的交互使用，将使得程序调试更为灵活便捷。例如：<BR><BR>set $i = 0<BR>print bar[$i++]-&gt;contents<BR><BR>于是，当你就不必，print bar[0]-&gt;contents, print bar[1]-&gt;contents地输入命令了。输入这样的命令后，只用敲回车，重复执行上一条语句，环境变量会自动累加，从而完成逐个输出的功能。<BR><BR><BR>十、查看寄存器<BR><BR>要查看寄存器的值，很简单，可以使用如下命令：<BR><BR>info registers <BR>查看寄存器的情况。（除了浮点寄存器）<BR><BR>info all-registers<BR>查看所有寄存器的情况。（包括浮点寄存器）<BR><BR>info registers <BR>查看所指定的寄存器的情况。<BR><BR>寄存器中放置了程序运行时的数据，比如程序当前运行的指令地址（ip），程序的当前堆栈地址（sp）等等。你同样可以使用print命令来访问寄存器的情况，只需要在寄存器名字前加一个$符号就可以了。如：p $eip。<BR><BR>改变程序的执行<BR>———————<BR><BR>一旦使用GDB挂上被调试程序，当程序运行起来后，你可以根据自己的调试思路来动态地在GDB中更改当前被调试程序的运行线路或是其变量的值，这个强大的功能能够让你更好的调试你的程序，比如，你可以在程序的一次运行中走遍程序的所有分支。<BR><BR><BR>一、修改变量值<BR><BR>修改被调试程序运行时的变量值，在GDB中很容易实现，使用GDB的print命令即可完成。如：<BR><BR>(gdb) print x=4<BR><BR>x=4这个表达式是C/C++的语法，意为把变量x的值修改为4，如果你当前调试的语言是Pascal，那么你可以使用Pascal的语法：x:=4。<BR><BR>在某些时候，很有可能你的变量和GDB中的参数冲突，如：<BR><BR>(gdb) whatis width<BR>type = double<BR>(gdb) p width<BR>$4 = 13<BR>(gdb) set width=47<BR>Invalid syntax in expression.<BR><BR>因为，set width是GDB的命令，所以，出现了“Invalid syntax in expression”的设置错误，此时，你可以使用set var命令来告诉GDB，width不是你GDB的参数，而是程序的变量名，如：<BR><BR>(gdb) set var width=47<BR><BR>另外，还可能有些情况，GDB并不报告这种错误，所以保险起见，在你改变程序变量取值时，最好都使用set var格式的GDB命令。<BR><BR><BR>二、跳转执行<BR><BR>一般来说，被调试程序会按照程序代码的运行顺序依次执行。GDB提供了乱序执行的功能，也就是说，GDB可以修改程序的执行顺序，可以让程序执行随意跳跃。这个功能可以由GDB的jump命令来完：<BR><BR>jump <BR>指定下一条语句的运行点。可以是文件的行号，可以是file:line格式，可以是+num这种偏移量格式。表式着下一条运行语句从哪里开始。<BR><BR>jump <BR><BR><BR>这里的<BR>是代码行的内存地址。<BR><BR>注意，jump命令不会改变当前的程序栈中的内容，所以，当你从一个函数跳到另一个函数时，当函数运行完返回时进行弹栈操作时必然会发生错误，可能结果还是非常奇怪的，甚至于产生程序Core Dump。所以最好是同一个函数中进行跳转。<BR><BR>熟悉汇编的人都知道，程序运行时，有一个寄存器用于保存当前代码所在的内存地址。所以，jump命令也就是改变了这个寄存器中的值。于是，你可以使用“set $pc”来更改跳转执行的地址。如：<BR><BR>set $pc = 0x485<BR><BR><BR>三、产生信号量<BR><BR>使用singal命令，可以产生一个信号量给被调试的程序。如：中断信号Ctrl+C。这非常方便于程序的调试，可以在程序运行的任意位置设置断点，并在该断点用GDB产生一个信号量，这种精确地在某处产生信号非常有利程序的调试。<BR><BR>语法是：signal ，UNIX的系统信号量通常从1到15。所以取值也在这个范围。<BR><BR>single命令和shell的kill命令不同，系统的kill命令发信号给被调试程序时，是由GDB截获的，而single命令所发出一信号则是直接发给被调试程序的。<BR><BR><BR>四、强制函数返回<BR><BR>如果你的调试断点在某个函数中，并还有语句没有执行完。你可以使用return命令强制函数忽略还没有执行的语句并返回。<BR><BR>return<BR>return <BR>使用return命令取消当前函数的执行，并立即返回，如果指定了，那么该表达式的值会被认作函数的返回值。<BR><BR><BR>五、强制调用函数<BR><BR>call <BR>表达式中可以一是函数，以此达到强制调用函数的目的。并显示函数的返回值，如果函数返回值是void，那么就不显示。<BR><BR>另一个相似的命令也可以完成这一功能——print，print后面可以跟表达式，所以也可以用他来调用函数，print和call的不同是，如果函数返回void，call则不显示，print则显示函数返回值，并把该值存入历史数据中。<BR><BR><BR>在不同语言中使用GDB<BR>——————————<BR><BR>GDB支持下列语言：C, C++, Fortran, PASCAL, Java, Chill, assembly, 和 Modula-2。一般说来，GDB会根据你所调试的程序来确定当然的调试语言，比如：发现文件名后缀为“.c”的，GDB会认为是C程序。文件名后缀为“.C, .cc, .cp, .cpp, .cxx, .c++”的，GDB会认为是C++程序。而后缀是“.f, .F”的，GDB会认为是Fortran程序，还有，后缀为如果是“.s, .S”的会认为是汇编语言。<BR><BR>也就是说，GDB会根据你所调试的程序的语言，来设置自己的语言环境，并让GDB的命令跟着语言环境的改变而改变。比如一些GDB命令需要用到表达式或变量时，这些表达式或变量的语法，完全是根据当前的语言环境而改变的。例如C/C++中对指针的语法是*p，而在Modula-2中则是p^。并且，如果你当前的程序是由几种不同语言一同编译成的，那到在调试过程中，GDB也能根据不同的语言自动地切换语言环境。这种跟着语言环境而改变的功能，真是体贴开发人员的一种设计。<BR><BR><BR>下面是几个相关于GDB语言环境的命令：<BR><BR>show language <BR>查看当前的语言环境。如果GDB不能识为你所调试的编程语言，那么，C语言被认为是默认的环境。<BR><BR>info frame<BR>查看当前函数的程序语言。<BR><BR>info source<BR>查看当前文件的程序语言。<BR><BR>如果GDB没有检测出当前的程序语言，那么你也可以手动设置当前的程序语言。使用set language命令即可做到。<BR><BR>当set language命令后什么也不跟的话，你可以查看GDB所支持的语言种类：<BR><BR>(gdb) set language<BR>The currently understood settings are:<BR><BR>local or auto Automatic setting based on source file<BR>c Use the C language<BR>c++ Use the C++ language<BR>asm Use the Asm language<BR>chill Use the Chill language<BR>fortran Use the Fortran language<BR>java Use the Java language<BR>modula-2 Use the Modula-2 language<BR>pascal Use the Pascal language<BR>scheme Use the Scheme language<BR><BR>于是你可以在set language后跟上被列出来的程序语言名，来设置当前的语言环境。<BR><BR><BR><BR>后记<BR>——<BR><BR>GDB是一个强大的命令行调试工具。大家知道命令行的强大就是在于，其可以形成执行序列，形成脚本。UNIX下的软件全是命令行的，这给程序开发提代供了极大的便利，命令行软件的优势在于，它们可以非常容易的集成在一起，使用几个简单的已有工具的命令，就可以做出一个非常强大的功能。<BR><BR>于是UNIX下的软件比Windows下的软件更能有机地结合，各自发挥各自的长处，组合成更为强劲的功能。而Windows下的图形软件基本上是各自为营，互相不能调用，很不利于各种软件的相互集成。在这里并不是要和Windows做个什么比较，所谓“寸有所长，尺有所短”，图形化工具还是有不如命令行的地方。（看到这句话时，希望各位千万再也不要认为我就是“鄙视图形界面”，和我抬杠了 ）<BR><BR>我是根据版本为5.1.1的GDB所写的这篇文章，所以可能有些功能已被修改，或是又有更为强劲的功能。而且，我写得非常仓促，写得比较简略，并且，其中我已经看到有许多错别字了（我用五笔，所以错字让你看不懂），所以，我在这里对我文中的差错表示万分的歉意。<BR><BR>文中所罗列的GDB的功能时，我只是罗列了一些带用的GDB的命令和使用方法，其实，我这里只讲述的功能大约只占GDB所有功能的60%吧，详细的文档，还是请查看GDB的帮助和使用手册吧，或许，过段时间，如果我有空，我再写一篇GDB的高级使用。<BR><BR>我个人非常喜欢GDB的自动调试的功能，这个功能真的很强大，试想，我在UNIX下写个脚本，让脚本自动编译我的程序，被自动调试，并把结果报告出来，调试成功，自动checkin源码库。一个命令，编译带着调试带着checkin，多爽啊。只是GDB对自动化调试目前支持还不是很成熟，只能实现半自动化，真心期望着GDB的自动化调试功能的成熟。<BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/1010.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-15 19:54 <a href="http://www.cnitblog.com/drizztzou/articles/1010.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>嵌入式操作系统的调试 </title><link>http://www.cnitblog.com/drizztzou/articles/752.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Tue, 05 Jul 2005 10:12:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/752.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/752.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/752.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/752.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/752.html</trackback:ping><description><![CDATA[调试是开发过程中必不可少的环节，通用的桌面操作系统与嵌入式操作系统在调试环境上存在明显的差别。前者，调试器与被调试的程序往往是运行在同一台机器、相同的操作系统上的两个进程，调试器进程通过操作系统专门提供的调用接口（早期UNIX系统的ptrace调用、如今的进程文件系统等）控制、访问被调试进程。后者（又称为远程调试），为了向系统开发人员提供灵活、方便的调试界面，调试器还是运行于通用桌面操作系统的应用程序，被调试的程序则运行于基于特定硬件平台的嵌入式操作系统（目标操作系统）。这就带来以下问题：调试器与被调试程序如何通信，被调试程序产生异常如何及时通知调试器，调试器如何控制、访问被调试程序，调试器如何识别有关被调试程序的多任务信息并控制某一特定任务，调试器如何处理某些与目标硬件平台相关的信息（如目标平台的寄存器信息、机器代码的反汇编等）。<BR><BR>我们介绍两种远程调试的方案，看它们怎样解决这些问题。<BR><BR>调试方案<BR>一 插桩（stub）<BR>第一种方案是在目标操作系统和调试器内分别加入某些功能模块，二者互通信息来进行调试。上述问题可通过以下途径解决：<BR><BR>1. 调试器与被调试程序的通信<BR>调试器与目标操作系统通过指定通信端口（串口、网卡、并口）遵循远程调试协议进行通信（远程调试协议详见<A href="http://rtos.ict.ac.cn/rtos/debugger/）。" target=_blank>http://rtos.ict.ac.cn/rtos/debugger/）。</A><BR>2. 被调试程序产生异常及时通知调试器<BR>目标操作系统的所有异常处理最终都要转向通信模块，告知调试器当前的异常号；调试器据此向用户显示被调试程序产生了哪一类异常。<BR>3. 调试器控制、访问被调试程序<BR>调试器的这类请求实际上都将转换成对被调试程序的地址空间或目标平台的某些寄存器的访问，目标操作系统接收到这样的请求可以直接处理。对于没有虚拟存储概念的简单的嵌入式操作系统而言，完成这些任务十分容易。<BR>4. 调试器识别有关被调试程序的多任务信息并控制某一特定任务<BR>由目标操作系统提供相关接口。目标系统根据调试器发送的关于多任务的请求，调用该接口提供相应信息或针对某一特定任务进行控制，并返回信息给调试器。<BR>5. 调试器处理与目标硬件平台相关的信息<BR>第2条所述调试器应能根据异常号识别目标平台产生异常的类型也属于这一范畴，这类工作完全可以由调试器独立完成。支持多种目标平台正是GNU GDB的一大特色。<BR><BR><BR><BR>综上所述，这一方案需要目标操作系统提供支持远程调试协议的通信模块（包括简单的设备驱动）和多任务调试接口，并改写异常处理的有关部分。另外目标操作系统还需要定义一个设置断点的函数；因为有的硬件平台提供能产生特定调试陷阱异常（debug trap）的断点指令以支持调试（如X86的INT 3），而另一些机器没有类似的指令，就用任意一条不能被解释执行的非法（保留）指令代替。目标操作系统添加的这些模块统称为"插桩"（见下图），驻留于ROM中则称为ROM monitor。通用操作系统也有具备这类模块的：编译运行于Alpha、Sparc或PowerPC平台的LINUX内核时若将kgdb开关打开，就相当于加入了插桩。<BR><BR>图1<BR><IMG onmouseover="if(this.alt) this.style.cursor='hand';" onclick="if(this.alt) window.open('http://www-900.ibm.com/developerWorks/cn/linux/embed/debug/fig1.gif');" src="http://www-900.ibm.com/developerWorks/cn/linux/embed/debug/fig1.gif" onload="if(this.width>screen.width*0.62) {this.width=screen.width*0.62;this.alt='点击查看全图';}" border=0><BR>图1<BR><BR>运行于目标操作系统的被调试的应用程序要在入口处调用这个设置断点的函数以产生异常，异常处理程序调用调试端口通信模块，等待主机（host）上的调试器发送信息。双方建立连接后调试器便等待用户发出调试命令，目标系统等待调试器根据用户命令生成的指令。这一过程如下图所示。<BR><BR>图2<BR><IMG onmouseover="if(this.alt) this.style.cursor='hand';" onclick="if(this.alt) window.open('http://www-900.ibm.com/developerWorks/cn/linux/embed/debug/fig2.gif');" src="http://www-900.ibm.com/developerWorks/cn/linux/embed/debug/fig2.gif" onload="if(this.width>screen.width*0.62) {this.width=screen.width*0.62;this.alt='点击查看全图';}" border=0><BR>图2<BR><BR>这一方案的实质是用软件接管目标系统的全部异常处理（exception handler）及部分中断处理，在其中插入调试端口通信模块，与主机的调试器交互。它只能在目标操作系统初始化，特别是调试通信端口初始化完成后才起作用，所以一般只用于调试运行于目标操作系统之上的应用程序，而不宜用来调试目标操作系统，特别是无法调试目标操作系统的启动过程。而且由于它必然要占用目标平台的某个通信端口，该端口的通信程序就无法调试了。最关键的是它必须改动目标操作系统，这一改动即使没有对操作系统在调试过程中的表现造成不利影响，至少也会导致目标系统多了一个不用于正式发布的调试版。<BR><BR>二 片上调试（On Chip Debugging）及Embedded PowerPC Background Debug Mode<BR>片上调试是在处理器内部嵌入额外的控制模块，当满足了一定的触发条件时进入某种特殊状态。在该状态下，被调试程序停止运行，主机的调试器可以通过处理器外部特设的通信接口访问各种资源（寄存器、存储器等）并执行指令。为了实现主机通信端口与目标板调试通信接口各引脚信号的匹配，二者往往通过一块简单的信号转换电路板连接（如下图所示）。内嵌的控制模块以基于微码的监控器（microcode monitor）或纯硬件资源的形式存在，包括一些提供给用户的接口（如断点寄存器等）。具体产品有Motorola CPU16、CPU32、Coldfire系列的BDM（Background Debug Mode），Motorola PowerPC 5xx、8xx系列的EPBDM（Embedded PowerPC Background Debug Mode），IBM、TI的JTAG（Joint Test Action Debug，IEEE标准），还有OnCE、MPSD等等。下面以MPC860的EPBDM为例介绍片上调试方式。<BR><BR>图3<BR><IMG onmouseover="if(this.alt) this.style.cursor='hand';" onclick="if(this.alt) window.open('http://www-900.ibm.com/developerWorks/cn/linux/embed/debug/fig3.gif');" src="http://www-900.ibm.com/developerWorks/cn/linux/embed/debug/fig3.gif" onload="if(this.width>screen.width*0.62) {this.width=screen.width*0.62;this.alt='点击查看全图';}" border=0><BR>图3<BR><BR>EPBDM的运作相当于用处理器内嵌的调试模块接管中断及异常处理。用户通过设置调试许可寄存器（debug enable register）来指定哪些中断或异常发生后处理器直接进入调试状态，而不是操作系统的处理程序。进入调试状态后，内嵌调试模块向外部调试通信接口发出信号，通知一直在通信接口监听的主机调试器，然后调试器便可通过调试模块使处理器执行任意系统指令（相当于特权态）。所有指令均通过调试模块获取，所有 load/store 均直接访问内存，缓存（cache）及存储管理单元（MMU）均不可用；数据寄存器被映射为一个特殊寄存器DPDR，通过mtspr和mfspr指令访问。调试器向处理器送rfi(return from interrupt)指令便结束调试状态，被调试程序继续运行。<BR><BR>与插桩方式的缺点相对应，OCD不占用目标平台的通信端口，无需修改目标操作系统，能调试目标操作系统的启动过程，大大方便了系统开发人员。随之而来的缺点是软件工作量的增加：调试器端除了需补充对目标操作系统多任务的识别、控制等模块，还要针对使用同一芯片的不同开发板编写各类ROM、RAM的初始化程序。<BR><BR>下面就以调试运行于MPC860的LINUX为例，说明用OCD方式调试OS 启动的某些关键细节。<BR><BR>首先，LINUX内核模块以压缩后的zImage形式驻留于目标板的ROM，目标板上电后先运行ROM中指定位置的程序将内核移至RAM并解压缩，然后再跳转至内核入口处运行。要调试内核，必须在上电后ROM中的指令执行之前获得系统的控制权，即进入调试状态、设断点，这样才能开展调试过程。MPC860 的EPBDM提供了这一手段。<BR><BR>MPC860没有类似X86的INT 3那样能产生特定调试陷阱异常的指令，而操作系统内核往往具有针对非法指令的异常处理；为了使对内核正常运行的干扰降至最小，调试时应尽量设置硬件断点，而不是利用非法指令产生异常的"软"断点。<BR><BR>LINUX实现了虚存管理，嵌入式LINUX往往也有这一功能。地址空间从实到虚的转换在内核启动过程中便完成了，不论调试内核还是应用程序，调试器都无法回避对目标系统虚地址空间的访问，否则断点命中时根本无法根据程序计数器的虚地址显示当前指令，更不用说访问变量了。由于调试状态下转换旁视缓冲器（Translation Lookaside Buffer）无法利用，只能仿照LINUX内核TLB失效时的异常处理程序，根据虚地址中的页表索引位访问特定寄存器查两级页表得出物理页面号，从而完成虚实地址的转换。MPC860采用哈佛结构（Harvard architecture），指令和数据缓存分离设置（因为程序的指令段和数据段是分离的，这种结构可以消除取指令和访问数据之间的冲突），二者的TLB 也分离设置；然而TLB失效时查找页表计算物理地址的过程是相同的，因为页表只有一个，不存在指令、数据分离的问题。虚实地址转换这一任务虽然完全落在了调试器一方，由于上述原因，再加上调试对象是嵌入式系统，一般不会有外存设备，不必考虑内存访问缺页的情况，所以增加的工作量并不大。<BR><BR>深入话题<BR>传统的调试方法可概括为如下过程：设断点--程序暂停--观察程序状态--继续运行。被调试的如果是实时系统，即使调试器支持批处理命令避免了用户输入命令、观察结果带来的延迟，它与目标系统之间的通信也完全可能错过对目标平台外设信号的响应。于是，针对某些调试器（如GDB）提供的监视点（trace point）这一特殊调试手段，目标方的插桩在原有的基础上被改进，称为代理（agent）。调试时用户首先在调试器设置监视点，以源代码表达式的形式指定感兴趣的对象名。为了减少代理解析表达式的工作，调试器将表达式转换为简单的字节码，传送至代理。程序运行后命中监视点、唤醒代理，代理根据字节码记录用户所需数据存入特定缓冲区（不仅仅是表达式的最终结果，还有中间结果），令程序继续运行；这一步骤无需与调试器通信。当调试器再度得到控制时，就可以发出命令，向代理查询历次监视记录。较之于插桩，代理增加了对接受到的字节码的分析模块，相应的目标代码体积只有大约3K字节；当然，监视记录缓冲区也要占用目标平台的存储空间，不过缓冲区的大小可在代理生成时由用户决定。总之，这一改进以有限的目标系统资源为代价，为实时监视提供了一个低成本的可行方案。<BR><BR>调试并不仅仅意味着设断点--程序暂停--观察--继续这一过程，往往还需要profiling、跟踪（trace）等多种手段，而现代微处理器的技术进步却为这些调试手段的实行带来了困难。以跟踪为例，其目的无非是记录真实的程序运行流；可现代处理器指令缓存都集成于芯片内（RISC处理器尤为如此），运行指令时"取指"这一操作大多在芯片内部针对指令缓存进行，芯片外部总线上只能观察到多条指令的预取（prefetch），预取的指令并不一定执行（由于跳转等原因）；另外，指令往往经过动态调度后在流水线中乱序执行，如何再现其原始顺序也是个问题。解决方案大致有以下三种：<BR><BR>1. 有的处理器除了正常运行外，还能以串行方式运行，所有的取指周期都可呈现于片外总线（相当于禁用缓存与流水线）。这样一来，跟踪容易多了，处理器性能也大大降低了，根本不适用于实时要求严格的系统。<BR>2. 编译器自动在指定的分支及函数出入口插入对特定内存区域的写指令（与gprof等profiling工具采用的手段类似），它们都是不通过缓存而直接向内存写的，这就能反映于芯片外总线从而被外接的逻辑分析仪记录，最终由主机端的调试工具分析并结合符号表重构程序流。这种方法虽被广泛使用，但毕竟是干扰式的（intrusive），对系统性能也有影响。<BR>3. 像上文所述的片上调试那样，也有处理器在片内附加了跟踪电路，收集程序流运行时的"不连贯"（discontinuities）信息（分支和异常处理的跳转目的及源地址等），压缩后送至特定端口，再由逻辑分析仪捕获送至主机端调试工具重构程序流。该方案对系统性能影响最小。<BR><BR>总之，处理器厂家提供集成于片内的调试电路为高档嵌入式系统开发提供各种非干扰式的调试手段早已是大势所趋。为了解决该领域标准化的需要，一些处理器厂家、工具开发公司和仪器制造商于1998年组成了Nexus 5001 Forum，这是一个旨在为嵌入式控制应用产生和定义嵌入式处理器调试接口标准的联合组织，以前的名称是Global Embedded Processor Debug Interface Standard Consortium（全球嵌入式处理器调试接口标准协会）。Nexus现在有24个成员单位，包括创始成员Motorola、Infineon Technologies、日立、ETAS和HP等公司。该组织首先处理的是汽车动力应用所需要的调试，现在已发展成为调试数据通信、无线系统和其他实时嵌入式应用的通用接口。<BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/752.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-05 18:12 <a href="http://www.cnitblog.com/drizztzou/articles/752.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux程式设计 </title><link>http://www.cnitblog.com/drizztzou/articles/737.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:54:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/737.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/737.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/737.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/737.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/737.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: &nbsp; 主题工具 vbmenu_register("threadtools");  &nbsp;  05-05-10, 18:16 &nbsp; 第&nbsp;1&nbsp;楼 langlang  vbmenu_register("postmenu_3935", true);  管理员 &nbsp...&nbsp;&nbsp;<a href='http://www.cnitblog.com/drizztzou/articles/737.html'>阅读全文</a><img src ="http://www.cnitblog.com/drizztzou/aggbug/737.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:54 <a href="http://www.cnitblog.com/drizztzou/articles/737.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>linux操作系统下c语言编程入门 </title><link>http://www.cnitblog.com/drizztzou/articles/734.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:46:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/734.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/734.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/734.html#Feedback</comments><slash:comments>2</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/734.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/734.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: (一)目录介绍 1)Linux程序设计入门--基础知识 2)Linux程序设计入门--进程介绍 3)Linux程序设计入门--文件操作 4)Linux程序设计入门--时间概念 5)Linux程序设计入门--信号处理 6)Linux程序设计入门--消息管理 7)Linux程序设计入门--线程操作 8)Linux程序设计入门--网络编程 9)Linux下C开发工具介绍 (二)具体内容 1)Linux程...&nbsp;&nbsp;<a href='http://www.cnitblog.com/drizztzou/articles/734.html'>阅读全文</a><img src ="http://www.cnitblog.com/drizztzou/aggbug/734.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:46 <a href="http://www.cnitblog.com/drizztzou/articles/734.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>emacs使用详解 </title><link>http://www.cnitblog.com/drizztzou/articles/733.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Mon, 04 Jul 2005 11:40:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/733.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/733.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/733.html#Feedback</comments><slash:comments>2</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/733.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/733.html</trackback:ping><description><![CDATA[GNU Emacs 是 Emacs, 的版本之一，是由 Emacs, 的原始版本(PDP-10)的作者Richard Stallman所编写的。 <BR><BR><BR>GNU Emacs的主要文档在GNU Emacs手册中，你可以用Info(Emacs的一个子系统)以行方式去读。请在那里查看完全的和最新的文档。本帮助页的更新依赖于志愿人员；Emacs 的维护人员的首要目标是使使用能本帮助页面尽可能少地占用其它更有用的项目的时间。 <BR><BR><BR>GNU Emacs 包含了其它 Emacs 编辑器所拥有的全部功能。由于它的编辑命令是用Lisp写的，因此很容易扩展。 <BR><BR><BR>Emacs 有一个扩展的交互帮助工具，但该工具假设你能熟练地使用 Emacs 窗口和缓冲区。CTRL-h (退格或CTRL-h) 可以进入该帮助工具。 Help Tutorial(帮助指南，CTRL-h t) 调用一个交互式的指南以帮助初学者在很短的时间内了解 Emacs 的基本知识。Help Apropos (CTRL-h a) 帮助你找到一个命令并给出其功能说明，Help Character (字符帮助，CTRL-h c) 描述一个给定的字符的作用，Help Function (函数帮助，CTRL-h f) 描述一个指定名称的Lisp 函数。 <BR><BR><BR><BR><BR>Emacs 的Undo(恢复功能)可以取消最近几步对缓冲区所做的修改，因此很容易从编辑错误中恢复 <BR><BR><BR><BR><BR>GNU Emacs 的许多特殊的包可以处理邮件读取(RMail)和邮件发送 <BR><BR><BR>(Mail)，大纲编辑 (Outline),编译(Compile), 在 Emacs 窗口中运行shell (Shell), 运行Lisp 的read-eval-print 循环(Lisp-Interaction-Mode), 以及自动修正(Doctor)。 <BR><BR><BR><BR><BR>还有一个扩充的参考手册，但是其它版本的 Emacs 用户即使没有它，要适应GNU Emacs也不会有什么困难。而Emacs的新用户通过学习指南和使用自身文档特性，很快就能够使用GNU Emacs的基本功能。 <BR><BR><BR><BR><BR>Emacs选项 <BR><BR><BR><BR><BR>以下的选项是很常用的： <BR><BR><BR><BR><BR>file <BR><BR><BR>编辑 文件。 <BR><BR><BR>+number <BR><BR><BR>转到由 number 指定的行(在"＋"和数字当中不要加入空格)。 <BR><BR><BR>-q <BR><BR><BR>不载入初始文件。 <BR><BR><BR>-u user <BR><BR><BR>载入 user 的初始文件。 <BR><BR><BR>-t file <BR><BR><BR>使用指定的 file 文件作为终端来代替stdin/stdout。该项必须作为命令行的第一个参数来说明。 <BR><BR><BR>以下的选项是面向lisp的 (这些选项按照给定的顺序处理)： <BR><BR><BR><BR><BR>-f function <BR><BR><BR>执行lisp函数 function 。 <BR><BR><BR>-l file <BR><BR><BR>装入文件 file 中的Lisp代码。 <BR><BR><BR>Emacs 作为批处理编辑器来运行时，以下选项是有用的： <BR><BR><BR><BR><BR>-batch <BR><BR><BR>批处理模式编辑。 编辑器将把消息送至stderr（注1）。这个选项必须位于参数列表的第一位。必须用-l和-f选项来指明要执行的文件和要调用的函数。 <BR><BR><BR>-kill <BR><BR><BR>在批处理模式下退出Emacs <BR><BR>Emacs 。 <BR><BR><BR>在X Window下使用Emacs <BR><BR><BR><BR><BR>Emacs 已经被设计得能在X Window系统下很好地工作。如果在X Window下运行 Emacs ，将创建其自己的X Window来显示。这是因为你也许会将Emacs编辑器作为后台进程来启动，并能够继续使用原来的窗口。 <BR><BR><BR><BR><BR>Emacs 在启动时能带以下X命令开关： <BR><BR><BR><BR><BR>-name name <BR><BR><BR>指定初始的 Emacs 窗口的名字。这使得能象窗口标题一样查询X资源。 <BR><BR><BR>-title name <BR><BR><BR>指定起始X窗口的标题。 <BR><BR><BR>-r <BR><BR><BR>反白显示 Emacs 窗口。 <BR><BR><BR>-i <BR><BR><BR>当 Emacs 窗口图标化时，使用"厨房水槽"位图图标。 <BR><BR><BR>-font font, -fn font <BR><BR><BR>将 Emacs 窗口字体设置为指定的字体 font 。在 /usr/lib/X11/fonts 路径下可以找到不同的 X 字体。注意： Emacs 只接受固定宽度的字体。在X11第四版字体命名协议下，若字体名的第11个域的值是"m"或"c"，则该字体是固定宽度的字体。此外，若字体名拥有 宽x高 的形式，它一般也是 固定 宽度字体。关于更多信息，参见xlsfonts（1）。 在指定字体时，别忘了在开关和字体名之间需要一个空格。 <BR><BR><BR>-b pixels <BR><BR><BR>设置 Emacs 窗口边框宽度为 pixels 指定的点数。缺省值是窗口每边宽为1点。 <BR><BR><BR>-ib pixels <BR><BR><BR>设置Emacs窗口内部边框宽度为 pixels 指定的点数。缺省值是窗口每边填充1点。 <BR><BR><BR><BR><BR>-geometry geometry <BR><BR><BR>按照给定的值设置 Emacs Emacs窗口的宽，高及位置。几何参数的指定应符合标准X格式，关于更多信息，参见 X（1） 。宽和高用字符来指定，缺省值是80 x 24。 <BR><BR><BR><BR><BR>-fg color <BR><BR><BR>在彩色显示下，设置文本的颜色。有效的颜色名列表见 /usr/lib/X11/rgb.txt 。 <BR><BR><BR>-bg color <BR><BR><BR>在彩色显示下，设置窗口背景的颜色。 <BR><BR><BR>-bd color <BR><BR><BR>在彩色显示下，设置窗口边框的颜色。 <BR><BR><BR>-cr color <BR><BR><BR>在彩色显示下，设置窗口文本光标的颜色。 <BR><BR><BR>-ms color <BR><BR><BR>在彩色显示下，设置窗口鼠标光标的颜色。 <BR><BR><BR>-d displayname, -display displayname <BR><BR><BR>在由 displayname. 指定的显示器上创建 Emacs 窗口。必须是在命令行中指定的第一个选项。 <BR><BR><BR>-nw <BR><BR><BR>告诉 Emacs 不使用其到 X 的特定界面。如果在从 xterm(1) 窗口调用 Emacs 时，使用此开关，则在该窗口中打开显示器。这必须是命令行中指定的第一个选项。 <BR><BR><BR>您可以在 .Xresources 文件（请参阅 xrdb(1) ）中为 Emacs 窗口设置 X 缺省值。请使用下列格式： <BR><BR><BR><BR><BR>emacs.keyword:value <BR><BR><BR>其中 value（值） 指定 keyword（关键字） 的缺省值。 Emacs 允许您为下列关键字设置缺省值： <BR><BR><BR><BR><BR>font (Font 类) <BR><BR><BR>设置窗口的文本 <BR><BR>痔濉? <BR><BR><BR>reverseVideo (ReverseVideo 类) <BR><BR><BR>如果把 reverseVideo 的值设置为 ，则反白显示窗口。 <BR><BR><BR>bitmapIcon ( BitmapIcon 类) <BR><BR><BR>如果 bitmapIcon 的值设置为 ，则窗口将图符化为凹入状。 <BR><BR><BR>borderWidth (BorderWidth 类) <BR><BR><BR>设置窗口的边框宽度（以 pixel 为单位）。 <BR><BR><BR>internalBorder (BorderWidth 类) <BR><BR><BR>设置窗口的内部边框宽度（以 pixel 为单位）。 <BR><BR><BR>foreground (Foreground 类) <BR><BR><BR>对于彩色显示器，设置窗口的文本颜色。 <BR><BR><BR>background (Background 类) <BR><BR><BR>对于彩色显示器，设置窗口的背景颜色。 <BR><BR><BR>borderColor (BorderColor 类) <BR><BR><BR>对于彩色显示器，设置窗口的边框颜色。 <BR><BR><BR>cursorColor (Foreground ? <BR><BR><BR>对于彩色显示器，设置窗口文本光标的颜色。 <BR><BR><BR>pointerColor (Foreground 类) <BR><BR><BR>对于彩色显示器，设置窗口鼠标光标的颜色。 <BR><BR><BR>geometry (Geometry 类) <BR><BR><BR>设置 Emacs 窗口的几何大小（如上所述）。 <BR><BR><BR>title (Title 类) <BR><BR><BR>设置 Emacs 窗口的标题。 <BR><BR><BR>iconName (Title 类) <BR><BR><BR>设置 Emacs 窗口图符的图符名称。 <BR><BR><BR>如果你在使用黑白显示器时试图设置颜色值，则窗口的特征将缺省为：前景色将设置为黑色，背景色将设置为白色，边框颜色将设置为灰色，而文本和鼠标光标将设置为黑色。 <BR><BR><BR><BR><BR>使用鼠标 <BR><BR><BR><BR><BR>下面列出在X11下 Emacs 窗口的鼠标按钮的捆绑功能。 <BR><BR><BR><BR><BR>左键 定点。 <BR><BR><BR><BR><BR><BR><BR>中键 粘贴文本 <BR><BR><BR><BR><BR><BR><BR>右键 把文本剪贴到X的剪贴缓冲区 <BR><BR><BR><BR><BR><BR><BR>SHIFT-中键 把文本剪贴到X的剪贴缓冲区。 <BR><BR><BR><BR><BR><BR><BR>SHIFT-右键 粘贴文本 <BR><BR><BR><BR><BR><BR><BR>CTRL-中键 把文本剪贴到X的剪贴缓冲区并把它删除 <BR><BR><BR><BR><BR><BR><BR>CTRL-右键 选择窗口并分割它成两个窗口,和输入CTRL-X 2一样 <BR><BR><BR><BR><BR><BR><BR>CTRL-SHIFT-左键 对X缓冲区菜单操作，挂起按钮和键盘，等待菜单出现， <BR><BR><BR>选择缓冲区和释放它。把鼠标移离菜单和释放操作。 <BR><BR><BR><BR><BR>CTRL-SHIFT-中键 弹出X菜单帮助索引Emacs帮助。 <BR><BR><BR><BR><BR><BR><BR>CTRL-SHIFT-右键 用鼠标选择窗口并删除所有其他窗口，和输入CTRL-X 1一样。 <BR><BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/733.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-07-04 19:40 <a href="http://www.cnitblog.com/drizztzou/articles/733.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux技巧小总结</title><link>http://www.cnitblog.com/drizztzou/articles/538.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Sat, 25 Jun 2005 14:53:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/538.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/538.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/538.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/538.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/538.html</trackback:ping><description><![CDATA[１、处理特殊的文件名<BR><BR>假设Linux系统中有一个文件名叫“-aaa”，如果我们想对它进行操作，例如现在要删除它，如果我们按照一般的删除方法在命令行中输入rm -aaa命令，界面将会提示我们是无效选项（invalid option），原来由于文件名的第一个字符为 - ，Linux把文件名当作选项了，我们可以使用“--”符号来解决这个问题，例如我们可以输入rm -- -aaa命令来删除-aaa文件。如果是其他特殊字符的话可以在特殊字符前加一个“”符号，或者用双引号把整个文件名括起来。<BR><BR>２、修复Linux下超级用户的密码<BR><BR>　　如果超级用户将密码忘记，就无法进入系统，也无法管理和使用系统。本来这种事不太可能发生，但是在一些Linux单机使用者，尤其是初学者中，却是比较容易发生。一般的解决方法就是格式化硬盘来重新安装系统，但这有点儿小题大作了。准备好bootdisk和rootdisk两张软盘，从软驱启动，启动到 root盘并出现shell提示符。将Linux根目录分区mount至/mnt目录，比如你的Linux在硬盘第一分区，就在命令行中输入mount /dev/hda1 /mnt，然后进入mnt目录，将其中的etc/passwd文件改名，输入mv /mnt/etc/passwd /mnt/etc/passwd.bak就可以了；接着使用命令cp /etc/passwd /mnt/etc/passwd将软盘上的/etc/passwd文件复制到硬盘中的etc目录下，这样重新由硬盘启动，登录时就不会询问超级密码；最后使用mv/etc/passwd.bak passwd命令将passwd文件改回，再运行passwd命令重新设定密码就可以了。<BR><BR>３、直接进行Linux的安装工作<BR><BR>通常我们在安装某个操作系统软件时，需要该系统的引导盘启动才能安装。但我们在安装Linux操作系统时，可以利用该系统光盘中的一个名为 loadlin.exe的DOS软件，将Linux核心直接调入内存，并由Linux核心代替当前操作系统来接管计算机，并进入Linux的安装界面。在安装Linux时，我们只要在运行对话框中输入loadlin E:imagesvmlinuz root=/dev/ram initrd=E:imagesinitrd.img这个命令就可以直接安装Linux了；其中E是光驱盘符，E:imagesvmlinuz为 Linux核心名。<BR><BR>４、快速启动Linux系统<BR><BR>随着个人计算机配置的日益提高，在自己的计算机上安装Linux系统已经不是什么新鲜的事了。假设我们的计算机上同时装有windows和Linux两个操作系统，应该如何启动Linux呢？是否每次都需要重启计算机，通过LiLo引导？其实如果我们在dos下，有一种简单快速启动Linux的方法，那就是load Linux。loadlin.exe是在dos下的可执行程序，它可以在纯dos环境下迅速启动Linux，而且无需重启计算机，通常我们可以在光盘的 /kernels目录下可以找到这个程序。如果不知这个程序被放置于安装盘的何处，可以使用“find －name loadlin＊”命令来寻找。找到之后将其复制到dos分区中，同时还需要复制一份你所使用的Linux内核文件。可以通过windows直接从光盘复制，也可在Linux环境下使用mcopy命令将文件copy到dos分区；接着再编写一个Linux.bat的批处理文件，文件内容如下：c: loadlin c:vmlinuz root=/dev/hda1 ro <BR><BR>　　其中我们假设loadlin.exe和vmlinuz这两个内核文件都在c盘根目录，而root为Linux根设备，而且Linux处于硬盘第一分区，所以设备名为/dev/hda1，ro意为readonly。以后在dos下要启动Linux时，运行Linux.bat就可以了。这样启动Linux快速高效，大大的减少了系统自检时等待的时间。 <BR><BR>５、消除Xwindows下的死机现象 <BR><BR>　　如果我们在Xwindows状态下运行Linux时，由于硬件本身的问题或者自己操作上的不当，有时侯可能会导致系统突然失去响应，也就是我们常见到的一种死机现象，其实此时系统并没有死机。我们可以用两个常用的方法来消除这种现象：第一，用键盘上的复合键Ctrl+Alt+Backspace来关闭当前正在运行的任务；第二，首先按住键盘上的Ctrl+Alt+F2复合键，让系统切换到另一个操作台，然后登陆到系统，再执行#ps -ax|grep startx命令，这将会列出你的Xserver的进程标识（PID），接着在命令行中输入如下命令就能消除Xwindows下的死机现象：#kill -9 PID_Number，最后通过Alt+F1复合键返回原来的平台。 <BR><BR>６、快速关闭Linux系统<BR><BR>　　旧版本的的Linux/UNIX系统必须先运行shutdown命令，然后才能关闭电源，但最新版本的Linux/UNIX系统已经在这个方面作了很大的改进，再也没有必要象以前那样关机了。因为新版本的系统借鉴了大型机的技术，采用了抗掉电的日志式文件系统，可以自动跟踪保存用户数据、自动同步刷新文件系统，用户完全可以随手关闭电源，从而达到快速关闭系统的目的了。 <BR><BR>７、巧妙使用rm命令<BR><BR>我们知道在Linux字符界面中，要删除一个文件的话可以使用rm命令，删除一个目录可以使用rmdir命令。但由于rmdir命令只能删除空白的目录，假使某个目录下面有文件，就只能先用rm命令来把目录中的文件删除掉才可以，所以通常需要rmdir与rm这两个命令配合使用才能彻底删除一个完整的目录。但用这种方法来对付几级子目录还能凑合，如果一个目录中含有若干个子目录，而且这若干子目录中又包含了若干级子目录，再使用这种方法不把你累死才怪。现在我们可以使用带-r参数的rm命令来删除一个非空目录，例如我们在命令行中输入rm -r bbb这样的命令，表示系统将把bbb目录中包含的所有文件和子目录全部删除掉。<BR><BR>８、善用虚拟操作台 <BR><BR>当我们登录进Linux系统后，如果再按一下键盘上的Alt+F2键，这时我们又可以看到一个Shell提示符,其实这个就是第二个虚拟操作台。通常，新安装的Linux系统共有四个虚拟操作台,我们可以分别用复合键Alt+F1、Alt+F2、Alt+F3、Alt+F4来访问它们。使用虚拟操作台频率最多的地方就是，当某一个程序出错锁住输入时或者Linux系统突然失去响应时，就可以切换到其他虚拟操作台登录进入后杀掉这个出错的进程任务。 <BR><BR>９、增加虚拟缓存 <BR><BR>如果计算机上的SWAP（交换空间）不够了，只要我们的硬盘上还有空余的空间，我们就可以把这些剩余空间利用起来，我们可以直接使用命令：mkswap /dev/hda（假设Linux的驱动器是/dev/hda），swapon /dev/hda；要自动启动Swap，可以把新的分区加入到etc/fstab中去，照着原来swap的写就行了。用"free"检查swap的大小， Linux支持最多16个交换分区，每个交换分区最大128M，没有空闲分区的时候，可以用个大文件来建立。下面是执行的一系列命令： <BR><BR>
<TABLE cellSpacing=0 cellPadding=5 width="100%" bgColor=#cccccc border=1>
<TBODY>
<TR>
<TD><PRE><CODE>#dd if=/dev/zero of=swapfile bs=1024 count=8192 <BR><BR>#mkswap swapfile 8192 <BR><BR>#sync <BR><BR>#swapon swapfile</CODE></PRE></TD></TR></TBODY></TABLE><BR><BR>１０、巧妙使用Tab键 <BR><BR>大家知道在Linux字符界面中输入命令时，有时需要输入很多字符，如果经常这样逐个逐个地输入字符，比较烦琐。假设键入的字符足以确定目录下一个唯一的文件时，我们只须按键盘上的 Tab 键就可以自动补齐该文件名的剩下部分，例如要把目录 /ccc 下的文件 ddddddd-1.2.3.tar.gz 解包时，当我们在命令行中键入到tar xvfz /ccc/d时，如果该文件是该目录下唯一以d起头的文件的话就可以直接按下键盘上的Tab键，这时命令会被自动补齐为：tar xvfz /ccc/ddddddd-1.2.3.tar.gz ，从而提高了输入效率。 <BR><BR>１１、多用拷贝与粘贴来提高操作速度 <BR><BR>Linux系统安装后，每次启动到字符界面时都会自动运行一个叫gpm的程序, 该程序运行后就可以用鼠标来拷贝与粘贴了。具体做法是按住鼠标左键拖动让要拷贝的地方突出显示, 这时突出显示的区域已经被拷贝, 再按鼠标右键拷贝的内容就会被粘贴在光标所在位置了。如果我们在Xwindow下运行Linux系统，拷贝与粘贴的操作与在普通的Win9x系统下一样。 <BR><BR>１２、加快Linux存取数据 <BR><BR>如果我们想快速提高Linux下的硬盘读取数据，可以在Linux下进行一些设置，让Linux在32位输入输出方式和DMA通道方式下进行工作。设置时，在Linux命令界面中输入命令/sbin/hdparm -cl /dev/hda来打开32bit传输方式，输入命令/sbin/hdparm -dl /dev/hda来打开DMA传输方式，接着再输入命令/sbin/hdparm -kl /dev/hda来使硬盘在Reset之后仍然保持上述的设置，通过这些设置，我们就能提高Linux的读盘速度到1倍以上。 <img src ="http://www.cnitblog.com/drizztzou/aggbug/538.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-25 22:53 <a href="http://www.cnitblog.com/drizztzou/articles/538.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>GCC 使用指南 </title><link>http://www.cnitblog.com/drizztzou/articles/536.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Sat, 25 Jun 2005 14:29:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/536.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/536.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/536.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/536.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/536.html</trackback:ping><description><![CDATA[GCC 使用指南 <BR><BR>使用语法： <BR>gcc [ option | filename ]... <BR>　　 g++ [ option | filename ]... <BR><BR>其中 option 为 gcc 使用时的选项(后面会再详述)， <BR>　　 而 filename 为欲以 gcc 处理的文件 <BR><BR>说明： <BR>这 C 与 C++ 的 compiler 已将产生新程序的相关程序整合起来。产 生一个新的程序需要经过四个阶段：预处理、编译、汇编、连结，而这两 个编译器都能将输入的文件做不同阶段的处理。虽然原始程序的扩展名可 用来分辨编写原始程序码所用的语言，但不同的compiler，其预设的处理 程序却各不相同： <BR><BR>gcc　　预设经由预处理过(扩展名为.i)的文件为 C 语言，并於程式 <BR>　　　　　 连结阶段以 C 的连结方式处理。 <BR><BR>g++　　预设经由预处理过(扩展名为.i)的文件为 C++ 语言，并於程序连结阶段以 C++ 的连结方式处理。 <BR><BR>原始程序码的扩展名指出所用编写程序所用的语言，以及相对应的处理方法： <BR><BR>　　 .c　　C 原始程序　　　　　　　　 ； 预处理、编译、汇编 <BR>　　 .C　　C++ 原始程序　　　　　　　　； 预处理、编译、汇编 <BR>　　 .cc C++ 原始程序　　　　　　　　； 预处理、编译、汇编 <BR>　　 .cxx C++ 原始程序　　　　　　　　； 预处理、编译、汇编 <BR>　　 .m　　Objective-C 原始程序　　　　； 预处理、编译、汇编 <BR>　　 .i　　已经过预处理之 C 原始程序　 ； 编译、汇编 <BR>　　 .ii 已经过预处理之 C++ 原始程序 ； 编译、汇编 <BR>　　 .s　　组合语言原始程序　　　　　　； 汇编 <BR>　　 .S　　组合语言原始程序　　　　　　； 预处理、汇编 <BR>　　 .h　　预处理文件(标头文件)　　　　； (不常出现在指令行) <BR><BR>其他扩展名的文件是由连结程序来处理，通常有： <BR>　　 .o　　Object file <BR>　　 .a　　Archive file <BR><BR>除非编译过程出现错误，否则 "连结" 一定是产生一个新程序的最 <BR>　　 後阶段。然而你也可以以 -c、-s 或 -E 等选项，将整个过程自四 <BR>　　 个阶段中的其中一个停止。在连结阶段，所有与原始码相对应的 <BR>　　 .o 文件、程序库、和其他无法自文件名辨明属性的文件(包括不以 .o <BR>　　 为扩展名的 object file 以及扩展名为 .a 的 archive file)都会 <BR>　　 交由连结程序来处理(在指令行将那些文件当作连结程序的参数传给 <BR>　　 连结程序)。 <BR><BR>选项： <BR><BR>　　 不同的选项必须分开来下：例如 `-dr' 这个选项就与 `-d -r' 大 <BR>　　 不相同。 <BR>　　 绝大部份的 `-f' 及 `-W' 选项都有正反两种形式：-fname 及 <BR>　　 -fno-name (或 -Wname 及 -Wno-name)。以下只列出非预设的那个 <BR>　　 形式。 <BR>　　 以下是所有选项的摘要。以形式来分类。选项的意义将另辟小节说 <BR>　　 明。 <BR><BR>　　 一般性(概略、常用的)选项 <BR>　　　　　　 -c -S -E -o file -pipe -v -x language <BR><BR>　　 程序语言选项 <BR>　　　　　　 -ansi -fall-virtual -fcond-mismatch <BR>　　　　　　 -fdollars-in-identifiers -fenum-int-equiv <BR>　　　　　　 -fexternal-templates -fno-asm -fno-builtin <BR>　　　　　　 -fno-strict-prototype -fsigned-bitfields <BR>　　　　　　 -fsigned-char -fthis-is-variable <BR>　　　　　　 -funsigned-bitfields -funsigned-char <BR>　　　　　　 -fwritable-strings -traditional -traditional-cpp <BR>　　　　　　 -trigraphs <BR><BR>　　 编译时的警告选项 <BR>　　　　　　 -fsyntax-only -pedantic -pedantic-errors -w -W <BR>　　　　　　 -Wall -Waggregate-return -Wcast-align -Wcast-qual <BR>　　　　　　 -Wchar-subscript -Wcomment -Wconversion <BR>　　　　　　 -Wenum-clash -Werror -Wformat -Wid-clash-len <BR>　　　　　　 -Wimplicit -Winline -Wmissing-prototypes <BR>　　　　　　 -Wmissing-declarations -Wnested-externs -Wno-import <BR>　　　　　　 -Wparentheses -Wpointer-arith -Wredundant-decls <BR>　　　　　　 -Wreturn-type -Wshadow -Wstrict-prototypes -Wswitch <BR>　　　　　　 -Wtemplate-debugging -Wtraditional -Wtrigraphs <BR>　　　　　　 -Wuninitialized -Wunused -Wwrite-strings <BR><BR>　　 除错选项 <BR>　　　　　　 -a -dletters -fpretend-float -g -glevel -gcoff <BR>　　　　　　 -gxcoff -gxcoff+ -gdwarf -gdwarf+ -gstabs -gstabs+ <BR>　　　　　　 -ggdb -p -pg -save-temps -print-file-name=library <BR>　　　　　　 -print-libgcc-file-name -print-prog-name=program <BR><BR>　 最佳化选项 <BR>　　　　　　 -fcaller-saves -fcse-follow-jumps -fcse-skip-blocks <BR>　　　　　　 -fdelayed-branch -felide-constructors <BR>　　　　　　 -fexpensive-optimizations -ffast-math -ffloat-store <BR>　　　　　　 -fforce-addr -fforce-mem -finline-functions <BR>　　　　　　 -fkeep-inline-functions -fmemoize-lookups <BR>　　　　　　 -fno-default-inline -fno-defer-pop <BR>　　　　　　 -fno-function-cse -fno-inline -fno-peephole <BR>　　　　　　 -fomit-frame-pointer -frerun-cse-after-loop <BR>　　　　　　 -fschedule-insns -fschedule-insns2 <BR>　　　　　　 -fstrength-reduce -fthread-jumps -funroll-all-loops <BR>　　　　　　 -funroll-loops -O -O2 <BR><BR>　　 预处理选项 <BR>　　　　　　 -Aassertion -C -dD -dM -dN -Dmacro[=defn] -E -H <BR>　　　　　　 -idirafter dir -include file -imacros file -iprefix <BR>　　　　　　 file -iwithprefix dir -M -MD -MM -MMD -nostdinc -P <BR>　　　　　　 -Umacro -undef <BR><BR>　　 汇编程序选项 <BR>　　　　　　 -Wa,option <BR>　　 连结程序选项 <BR>　　　　　　 -llibrary -nostartfiles -nostdlib -static -shared <BR>　　　　　　 -symbolic -Xlinker option -Wl,option -u symbol 
<P>　　目录选项 <BR>　　　　　　 -Bprefix -Idir -I- -Ldir <BR><BR>　 Target Options <BR>　　　　　　 -b machine -V version <BR><BR>　 与机器(平台)相关的选项 <BR>　　　　　　 M680x0 Options <BR>　　　　　　 -m68000 -m68020 -m68020-40 -m68030 -m68040 -m68881 <BR>　　　　　　 -mbitfield -mc68000 -mc68020 -mfpa -mnobitfield <BR>　　　　　　 -mrtd -mshort -msoft-float <BR><BR>VAX Options <BR>　　　　　　 -mg -mgnu -munix <BR><BR>SPARC Options <BR>　　　　　　 -mepilogue -mfpu -mhard-float -mno-fpu <BR>　　　　　　 -mno-epilogue -msoft-float -msparclite -mv8 <BR>　　　　　　 -msupersparc -mcypress <BR><BR>Convex Options <BR>　　　　　　 -margcount -mc1 -mc2 -mnoargcount <BR><BR>AMD29K Options <BR>　　　　　　 -m29000 -m29050 -mbw -mdw -mkernel-registers <BR>　　　　　　 -mlarge -mnbw -mnodw -msmall -mstack-check <BR>　　　　　　 -muser-registers <BR><BR>M88K Options <BR>　　　　　　 -m88000 -m88100 -m88110 -mbig-pic <BR>　　　　　　 -mcheck-zero-division -mhandle-large-shift <BR>　　　　　　 -midentify-revision -mno-check-zero-division <BR>　　　　　　 -mno-ocs-debug-info -mno-ocs-frame-position <BR>　　　　　　 -mno-optimize-arg-area -mno-serialize-volatile <BR>　　　　　　 -mno-underscores -mocs-debug-info <BR>　　　　　　 -mocs-frame-position -moptimize-arg-area <BR>　　　　　　 -mserialize-volatile -mshort-data-num -msvr3 -msvr4 <BR>　　　　　　 -mtrap-large-shift -muse-div-instruction <BR>　　　　　　 -mversion-03.00 -mwarn-passed-structs <BR><BR>　 RS6000 Options <BR>　　　　　　 -mfp-in-toc -mno-fop-in-toc <BR><BR>RT Options <BR>　　　　　　 -mcall-lib-mul -mfp-arg-in-fpregs -mfp-arg-in-gregs <BR>　　　　　　 -mfull-fp-blocks -mhc-struct-return -min-line-mul <BR>　　　　　　 -mminimum-fp-blocks -mnohc-struct-return <BR><BR>MIPS Options <BR>　　　　　　 -mcpu=cpu type -mips2 -mips3 -mint64 -mlong64 <BR>　　　　　　 -mlonglong128 -mmips-as -mgas -mrnames -mno-rnames <BR>　　　　　　 -mgpopt -mno-gpopt -mstats -mno-stats -mmemcpy <BR>　　　　　　 -mno-memcpy -mno-mips-tfile -mmips-tfile <BR>　　　　　　 -msoft-float -mhard-float -mabicalls -mno-abicalls <BR>　　　　　　 -mhalf-pic -mno-half-pic -G num -nocpp <BR><BR>i386 Options <BR>　　　　　　 -m486 -mno-486 -msoft-float -mno-fp-ret-in-387 <BR><BR>HPPA Options <BR>　　　　　　 -mpa-risc-1-0 -mpa-risc-1-1 -mkernel -mshared-libs <BR>　　　　　　 -mno-shared-libs -mlong-calls -mdisable-fpregs <BR>　　　　　　 -mdisable-indexing -mtrailing-colon <BR><BR>　 i960 Options <BR>　　　　　　 -mcpu-type -mnumerics -msoft-float <BR>　　　　　　 -mleaf-procedures -mno-leaf-procedures -mtail-call <BR>　　　　　　 -mno-tail-call -mcomplex-addr -mno-complex-addr <BR>　　　　　　 -mcode-align -mno-code-align -mic-compat <BR>　　　　　　 -mic2.0-compat -mic3.0-compat -masm-compat <BR>　　　　　　 -mintel-asm -mstrict-align -mno-strict-align <BR>　　　　　　 -mold-align -mno-old-align <BR><BR>　 DEC Alpha Options <BR>　　　　　　 -mfp-regs -mno-fp-regs -mno-soft-float -msoft-float <BR><BR>　 System V Options <BR>　　　　　　 -G -Qy -Qn -YP,paths -Ym,dir <BR><BR>　 Code Generation Options <BR>　　　　　　 -fcall-saved-reg -fcall-used-reg -ffixed-reg <BR>　　　　　　 -finhibit-size-directive -fnonnull-objects <BR>　　　　　　 -fno-common -fno-ident -fno-gnu-linker <BR>　　　　　　 -fpcc-struct-return -fpic -fPIC <BR>　　　　　　 -freg-struct-returno -fshared-data -fshort-enums <BR>　　　　　　 -fshort-double -fvolatile -fvolatile-global <BR>　　　　　　 -fverbose-asm <BR><BR>PRAGMAS <BR>　　 Two `#pragma' directives are supported for GNU C++, to <BR>　　 permit using the same header file for two purposes: as a <BR>　　 definition of interfaces to a given object class, and as <BR>　　 the full definition of the contents of that object class. <BR><BR>　　 #pragma interface <BR>　　　　　　 (C++ only.) Use this directive in header files <BR>　　　　　　 that define object classes, to save space in most <BR>　　　　　　 of the object files that use those classes. Nor- <BR>　　　　　　 mally, local copies of certain information (backup <BR>　　　　　　 copies of inline member functions, debugging infor- <BR>　　　　　　 mation, and the internal tables that implement vir- <BR>　　　　　　 tual functions) must be kept in each object file <BR>　　　　　　 that includes class definitions. You can use this <BR>　　　　　　 pragma to avoid such duplication. When a header <BR>　　　　　　 file containing `#pragma interface' is included in <BR>　　　　　　 a compilation, this auxiliary information will not <BR>　　　　　　 be generated (unless the main input source file it- <BR>　　　　　　 self uses `#pragma implementation'). Instead, the <BR>　　　　　　 object files will contain references to be resolved <BR>　　　　　　 at link time. <BR><BR>　　 #pragma implementation <BR><BR>　　 #pragma implementation "objects.h" <BR>　　　　　　 (C++ only.) Use this pragma in a main input file, <BR>　　　　　　 when you want full output from included header <BR>　　　　　　 files to be generated (and made globally visible). <BR>　　　　　　 The included header file, in turn, should use <BR>　　　　　　 `#pragma interface'. Backup copies of inline mem- <BR>　　　　　　 ber functions, debugging information, and the in- <BR>　　　　　　 ternal tables used to implement virtual functions <BR>　　　　　　 are all generated in implementation files. <BR><BR>　　　　　　 If you use `#pragma implementation' with no argu- <BR>　　　　　　 ment, it applies to an include file with the same <BR>　　　　　　 basename as your source file; for example, in <BR>　　　　　　 `allclass.cc', `#pragma implementation' by itself <BR>　　　　　　 is equivalent　　to　　`#pragma　　implementation <BR>　　　　　　 "allclass.h"'. Use the string argument if you want <BR>　　　　　　 a single implementation file to include code from <BR>　　　　　　 multiple header files. <BR><BR>　　　　　　 There is no way to split up the contents of a sin- <BR>　　　　　　 gle header file into multiple implementation files. <BR><BR>文件说明 <BR>　　 file.c　　　　　　 C source file <BR>　　 file.h　　　　　　 C header (preprocessor) file <BR>　　 file.i　　　　　　 经预处理过的 C source file <BR>　　 file.C　　　　　　 C++ source file <BR>　　 file.cc　　　　　　C++ source file <BR>　　 file.cxx　　　 　C++ source file <BR>　　 file.m　　　　　　 Objective-C source file <BR>　　 file.s　　　　　　 assembly language file <BR>　　 file.o　　　　　　 object file <BR>　　 a.out　　　　　　 link edited output <BR>　　 TMPDIR/cc*　　　　 temporary files <BR>　　 LIBDIR/cpp　　　　 preprocessor <BR>　　 LIBDIR/cc1　　　　 compiler for C <BR>　　 LIBDIR/cc1plus　　 compiler for C++ <BR>　　 LIBDIR/collect　　 linker front end needed on some machines <BR>　　 LIBDIR/libgcc.a　　GCC subroutine library <BR>　　 /lib/crt[01n].o　　start-up routine <BR>　　 LIBDIR/ccrt0　　additional start-up routine for C++ <BR>　　 /lib/libc.a　　　　standard C library, 参阅 man page intro(3) <BR>　　 /usr/include　　standard directory for #include files <BR>　　 LIBDIR/include　　 standard gcc directory for #include files <BR>　　 LIBDIR/g++-include additional g++ directory for #include <BR><BR>　　 LIBDIR is usually /usr/local/lib/machine/version. <BR>　　 TMPDIR comes from the environment variable TMPDIR (default <BR>　　 /usr/tmp if available, else /tmp). <BR></P><img src ="http://www.cnitblog.com/drizztzou/aggbug/536.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-25 22:29 <a href="http://www.cnitblog.com/drizztzou/articles/536.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>什么是module 以及如何写一个module</title><link>http://www.cnitblog.com/drizztzou/articles/503.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 17:01:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/503.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/503.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/503.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/503.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/503.html</trackback:ping><description><![CDATA[不知道在什幺时候，Linux 出现了 module 这种东西，的确，它是 Linux 的一大革新。有了 module 之后，写 device driver 不再是一项恶梦，修改 kernel 也不再是一件痛苦的事了。因为你不需要每次要测试 driver 就重新 compile kernel 一次。那简直是会累死人。Module 可以允许我们动态的改变 kernel，加载 device driver，而且它也能缩短我们 driver development 的时间。在这篇文章里，我将要跟各位介绍一下 module 的原理，以及如何写一个 module。 <BR><BR>module 翻译成中文就是模块，不过，事实上去翻译这个字一点都没意义。在讲模块之前，我先举一个例子。相信很多人都用过 RedHat。在 RedHat 里，我们可以执行 sndconfig，它可以帮我们 config 声卡。config 完之后如果捉得到你的声卡，那你的声卡马上就可以动了，而且还不用重新激活计算机。这是怎幺做的呢 ? 就是靠module。module 其实是一般的程序。但是它可以被动态载到 kernel 里成为 kernel的一部分。载到 kernel 里的 module 它具有跟 kernel 一样的权力。可以 access 任何 kernel 的 data structure。你听过 kdebug 吗 ? 它是用来 debug kernel 的。它就是先将它本身的一个 module 载到 kernel 里，而在 user space 的 gdb 就可以经由跟这个 module 沟通，得知 kernel 里的 data structure 的值，除此之外，还可以经由载到 kernel 的 module 去更改 kernel 里 data structure。 <BR><BR>我们知道，在写 C 程序的时候，一个程序只能有一个 main。Kernel 本身其实也是一个程序，它本身也有个 main，叫 start_kernel()。当我们把一个 module 载到 kernel 里的时候，它会跟 kernel 整合在一起，成为 kernel 的一部分。请各位想想，那 module 可以有 main 吗 ? 答案很明显的，是 No。理由很简单。一个程序只能有一个 main。在使用 module 时，有一点要记住的是 module 是处于被动的角色。它是提供某些功能让别人去使用的。 <BR><BR>Kernel 里有一个变量叫 module_list，每当 user 将一个 module 载到 kernel 里的时候，这个 module 就会被记录在 module_list 里面。当 kernel 要使用到这个 module 提供的 function 时，它就会去 search 这个 list，找到 module，然后再使用其提供的 function 或 variable。每一个 module 都可以 export 一些 function 或变量来让别人使用。除此之外，module 也可以使用已经载到 kernel 里的 module 提供的 function。这种情形叫做 module stack。比方说，module A 用到 module B 的东西，那在加载 module A 之前必须要先加载 module B。否则 module A 会无法加载。除了 module 会 export 东西之外，kernel 本身也会 export 一些 function 或 variable。同样的，module 也可以使用 kernel 所 export 出来的东西。由于大家平时都是撰写 user space 的程序，所以，当突然去写 module 的时候，会把平时写程序用的 function 拿到 module 里使用。像是 printf 之类的东西。我要告诉各位的是，module 所使用的 function 或 variable，要嘛就是自己写在 module 里，要嘛就是别的 module 提供的，再不就是 kernel 所提供的。你不能使用一般 libc 或 glibc所提供的 function。像 printf 之类的东西。这一点可能是各位要多小心的地方。(也许你可以先 link 好，再载到 kernel，我好象试过，但是忘了) <BR><BR>刚才我们说到 kernel 本身会 export 出一些 function 或 variable 来让 module 使用，但是，我们不是万能的，我们怎幺知道 kernel 有开放那里东西让我们使用呢 ? Linux 提供一个 command，叫 ksyms，你只要执行 ksyms -a 就可以知道 kernel 或目前载到 kernel 里的 module 提供了那些 function 或 variable。底下是我的系统的情形: <BR><BR>c0216ba0 drive_info_R744aa133 <BR>c01e4a44 boot_cpu_data_R660bd466 <BR>c01e4ac0 EISA_bus_R7413793a <BR>c01e4ac4 MCA_bus_Rf48a2c4c <BR>c010cc34 __verify_write_R203afbeb <BR>. . . . . <BR><BR>在 kernel 里，有一个 symbol table 是用来记录 export 出去的 function 或 variable。除此之外，也会记录着那个 module export 那些 function。上面几行中，表示 kernel 提供了 drive_info 这个 function/variable。所以，我们可以在 kernel 里直接使用它，等载到 kernel 里时，会自动做好 link 的动作。由此，我们可以知道，module 本身其实是还没做 link 的一些 object code。一切都要等到 module 被加载 kernel 之后，link 才会完成。各位应该可以看到 drive_info 后面还接着一些奇怪的字符串。_R744aa133，这个字符串是根据目前 kernel 的版本再做些 encode 得出来的结果。为什幺额外需要这一个字符串呢 ? <BR><BR>Linux 不知道从那个版本以来，就多了一个 config 的选项，叫做 Set version number in symbols of module。这是为了避免对系统造成不稳定。我们知道 Linux 的 kernel 更新的很快。在 kernel 更新的过程，有时为了效率起见，会对某些旧有的 data structure 或 function 做些改变，而且一变可能有的 variable 被拿掉，有的 function 的 prototype 跟原来的都不太一样。如果这种情形发生的时候，那可能以前 2.0.33 版本的 module 拿到 2.2.1 版本的 kernel 使用，假设原来 module 使用了 2.0.33 kernel 提供的变量叫 A，但是到了 2.2.1 由于某些原因必须把 A 都设成 NULL。那当此 module 用在 2.2.1 kernel 上时，如果它没去检查 A 的值就直接使用的话，就会造成系统的错误。也许不会整个系统都死掉，但是这个 module 肯定是很难发挥它的功能。为了这个原因，Linux 就在 compile module 时，把 kernel 版本的号码 encode 到各个 exported function 和 variable 里。 <BR><BR>所以，刚才也许我们不应该讲 kernel 提供了 drive_info，而应该说 kernel 提供了 driver_info_R744aa133 来让我们使用。这样也许各位会比较明白。也就是说，kernel 认为它提供的 driver_info_R744aa133 这个东西，而不是 driver_info。所以，我们可以发现有的人在加载 module 时，系统都一直告诉你某个 function 无法 resolved。这就是因为 kernel 里没有你要的 function，要不然就是你的 module 里使用的 function 跟 kernel encode 的结果不一样。所以无法 resolve。解决方式，要嘛就是将 kernel 里的 set version 选项关掉，要嘛就是将 module compile 成 kernel 有办法接受的型式。 <BR><BR>那有人就会想说，如果 kernel 认定它提供的 function 名字叫做 driver_info_R744aa133 的话，那我们写程序时，是不是用到这个 funnction 的地方都改成 driver_info_R744aa133 就可以了。答案是 Yes。但是，如果每个 function 都要你这样写，你不会觉得很烦吗 ? 比方说，我们在写 driver 时，很多人都会用到 printk 这个 function。这是 kernel 所提供的 function。它的功能跟 printf 很像。用法也几乎都一样。是 debug 时很好用的东西。如果我们 module 里用了一百次 printk，那是不是我们也要打一百次的 printk_Rdd132261 呢 ? 当然不是，聪明的人马上会想到用 #define printk printk_Rdd132261 就好了嘛。所以啰，Linux 很体贴的帮我们做了这件事。 <BR><BR>如果各位的系统有将 set version 的选项打开的话，那大家可以到 /usr/src/linux/include/linux/modules 这个目录底下。这个目录底下有所多的 ..ver档案。这些档案其实就是用来做 #define 用的。我们来看看 ksyms.ver 这个档案里，里面有一行是这样子的 : <BR><BR>#define printk _set_ver(printk) <BR><BR>set_ver 是一个 macro，就是用来在 printk 后面加上 version number 的。有兴趣的朋友可以自行去观看这个 macro 的写法。用了这些 ver 檔，我们就可以在 module 里直接使用 printk 这样的名字了。而这些 ver 档会自动帮我们做好 #define 的动作。可是，我们可以发现这个目录有很多很多的 ver 檔。有时候，我们怎幺知道我们要呼叫的 function 是在那个 ver 档里有定义呢 ? Linux 又帮我们做了一件事。/usr/src/linux/include/linux/modversions.h 这个档案已经将全部的 ver 档都加进来了。所以在我们的 module 里只要 include 这个档，那名字的问题都解决了。但是，在此，我们奉劝各位一件事，不要将 modversions.h 这个档在 module 里 include 进来，如果真的要，那也要加上以下数行: <BR><BR>#ifdef MODVERSIONS <BR>#include <LINUX modversions.h><BR>#endif <BR><BR>加入这三行的原因是，避免这个 module 在没有设定 kernel version 的系统上，将 modversions.h 这个档案 include 进来。各位可以去试试看，当你把 set version 的选项关掉时，modversions.h 和 modules 这个目录都会不见。如果没有上面三行，那 compile 就不会过关。所以一般来讲，modversions.h 我们会选择在 compile 时传给 gcc 使用。就像下面这个样子。 <BR><BR>gcc -c -D__KERNEL__ -DMODULE -DMODVERSIONS main.c \\ <BR>-include usr/src/linux/include/linux/modversions.h <BR><BR>在这个 command line 里，我们看到了 -D__KERNEL__，这是说要定义 __KERNEL__ 这个 constant。很多跟 kernel 有关的 header file，都必须要定义这个 constant 才能 include 的。所以建议你最好将它定义起来。另外还有一个 -DMODVERSIONS。这个 constant 我刚才忘了讲。刚才我们说要解决 fucntion 或 variable 名字 encode 的方式就是要 include modversions.h，其实除此之外，你还必须定义 MODVERSIONS 这个 constant。再来就是 MODULE 这个 constant。其实，只要是你要写 module 就一定要定义这个变量。而且你还要 include module.h 这个档案，因为 _set_ver 就是定义在这里的。 <BR><BR>讲到这里，相信各位应该对 module 有一些认识了，以后遇到 module unresolved 应该不会感到困惑了，应该也有办法解决了。 <BR><BR>刚才讲的都是使用别人的 function 上遇到的名字 encode 问题。但是，如果我们自己的 module 想要 export 一些东西让别的 module 使用呢。很简单。在 default 上，在你的 module 里所有的 global variable 和 function 都会被认定为你要 export 出去的。所以，如果你的 module 里有 10 个 global variable，经由 ksyms，你可以发现这十个 variable 都会被 export 出去。这当然是个很方便的事啦，但是，你知道，有时候我们根本不想把所有的 variable 都 export 出去，万一有个 module 没事乱改我们的 variable 怎幺办呢 ? 所以，在很多时候，我们都只会限定几个必要的东西 export 出去。在 2.2.1 之前的 kernel (不是很确定) 可以利用 register_symtab 来帮我们。但是，现在更新的版本早就出来了。所以，在此，我会介绍 kernel 2.2.1 里所提供的。kernel 2.2.1 里提供了一个 macro，叫做 EXPORT_SYMBOL，这是用来帮我们选择要 export 的 variable 或 function。比方说，我要 export 一个叫 full 的 variable，那我只要在 module 里写: <BR><BR>EXPORT_SYMBOL(full); <BR><BR>就会自动将 full export 出去，你马上就可以从 ksyms 里发现有 full 这个变量被 export 出去。在使用 EXPORT_SYMBOL 之前，要小心一件事，就是必须在 gcc 里定义 EXPORT_SYMTAB 这个 constant，否则在 compile 时会发生 parser error。所以，要使用 EXPORT_SYMBOL 的话，那 gcc 应该要下: <BR><BR>gcc -c -D__KERNEL__ -DMODULE -DMODVERSIONS -DEXPORT_SYMTAB \\ <BR>main.c -include /usr/src/linux/include/linux/modversions.h <BR><BR>如果我们不想 export 任何的东西，那我们只要在 module 里下 <BR><BR>EXPORT_NO_SYMBOLS; <BR><BR>就可以了。使用 EXPORT_NO_SYMBOLS 用不着定义任何的 constant。其实，如果各位使用过旧版的 register_symbol 的话，一定会觉得新版的方式比较好用。至少我是这样觉得啦。因为使用 register_symbol 还要先定义出自己的 symbol_table，感觉有点麻烦。 <BR><BR>当我们使用 EXPORT_SYMBOL 把一些 function 或 variable export 出来之后，我们使用 ksyma -a 去看一些结果。我们发现 EXPORT_SYMBOL(full) 的确是把 full export出来了 : <BR><BR>c8822200 full [my_module] <BR>c01b8e08 pci_find_slot_R454463b5 <BR>. . . <BR><BR>但是，结果怎幺跟我们想象中的不太一样，照理说，应该是 full_Rxxxxxx 之类的东西才对啊，怎幺才出现 full 而已呢 ? 奇怪，问题在那里呢 ? <BR><BR>其实，问题就在于我们没有对本身的 module 所 export 出来的 function 或 variable 的名字做 encode。想想，如果在 module 的开头。我们加入一行 <BR><BR>#define full full_Rxxxxxx <BR><BR>之后，我们再重新 compile module 一次，载到 kernel 之后，就可以发现 ksyms -a 显示的是 <BR><BR>c8822200 full_Rxxxxxx [my_module] <BR>c01b8e08 pci_find_slot_R454463b5 <BR>. . . . . <BR><BR>了。那是不是说，我们要去对每一个 export 出来的 variable 和 function 做 define 的动作呢 ? 当然不是啰。记得吗，前头我们讲去使用 kernel export 的 function 时，由于 include 了一些 .ver 的档案，以致于我们不用再做 define 的动作。现在，我们也要利用 .ver 的档案来帮我们，使我们 module export 出来的 function 也可以自动加入 kernel version 的 information。也就是变成 full_Rxxxxxx 之类的东西。 <BR><BR>Linux 里提供了一个 command，叫 genksyms，就是用来帮我们产生这种 .ver 的档案的。它会从 stdin 里读取 source code，然后检查 source code 里是否有 export 的 variable 或 function。如果有，它就会自动为每个 export 出来的东西产生一些 define。这些 define 就是我们之前说的。等我们有了这些 define 之后，只要在我们的 module 里加入这些 define，那 export 出来的 function 或 variable 就会变成上面那个样子。 <BR><BR>假设我们的程序都放在一个叫 main.c 的档案里，我们可以使用下列的方式产生这些 define。 <BR><BR>gcc -E -D__GENKSYMS__ main.c | genksyms -k 2.2.1 &gt; main.ver <BR><BR>gcc 的 -E 参数是指将 preprocessing 的结果 show 出来。也就是说将它 include 的档案，一些 define 的结果都展开。-D__GENKSYMS__ 是一定要的。如果没有定义这个 constant，你将不会看到任何的结果。用一个管线是因为 genksyms 是从 stdin 读资料的，所以，经由管线将 gcc 的结果传给 genksyms。-k 2.2.1 是指目前使用的 kernel 版本是 2.2.1，如果你的 kernel 版本不一样，必须指定你的 kernel 的版本。产生的 define 将会被放到 main.ver 里。产生完 main.ver 档之后，在 main.c 里将它 include 进来，那一切就 OK 了。有件事要告诉各位的是，使用这个方式产生的 module，其 export 出来的东西会经由 main.ver 的 define 改头换面。所以如果你要让别人使用，那你必须将 main.ver 公开，不然，别人就没办法使用你 export 出来的东西了。 <BR><BR>讲了这幺多，相信各位应该都已经比较清楚 module 在 kernel 中是怎幺样一回事，也应该知道为什幺有时候 module 会无法加载了。除此之外，各位应该还知道如何使自己 module export 出来的东西也具有 kernel version 的 information。 <BR><BR>接下来，要跟各位讲的就是，如何写一个 module 了。其实，写一个 module 很简单的。如果你了解我上面所说的东西。那我再讲一次，再用个例子，相信大家就都会了。要写一个 module，必须要提供两个 function。这两个 function 是给 insmod 和 rmmod 使用的。它们分别是 init_module()，以及 cleanup_module()。 <BR><BR>int init_module(); <BR>void cleanup_module(); <BR><BR>相信大家都知道在 Linux 里可以使用 insmod 这个 command 来将某个 module 加载。比方说，我有一个 module 叫 hello.o，那使用 insmod hello.o 就可以将 hello 这个 module 载到 kernel 里。观察 /etc/modules 应该就可以看到 hello 这个 module 的名字。如果要将 hello 这个 module 移除，则只要使用 rmmod hello 就可以了。insmod 在加载 module 之后，就会去呼叫 module 所提供的 init_module()。如果传回 0 表示成功，那 module 就会被加载。如果失败，那加载的动作就会失败。一般来讲，我们在 init_module() 做的事都是一些初始化的工作。比方说，你的 module 需要一块内存，那你就可以在 init_module() 做 kmalloc 的动作。想当然尔。cleanup_module() 就是在 module 要移除的时候做的事。做的事一般来讲就是一些善后的工作，比方像把之前 kmalloc 的内存 free 掉。 <BR><BR>由于 module 是载到 kernel 使用的，所以，可能别的 module 会使用你的 module，甚至某些 process 也会使用到你的 module，为了避免 module 还有人使用时就被移除，每个 module 都有一个 use count。用来记录目前有多少个 process 或 module 正在使用这个 module。当 module 的 use count 不等于 0 时，module 是不会被移除掉的。也就是说，当 module 的 use count 不等于 0 时，cleanup_module() 是不会被呼叫的。 <BR><BR>在此，我要介绍三个 macro，是跟 module 的 use count 有关的。 <BR><BR>MOD_INC_USE_COUNT <BR>MOD_DEC_USE_COUNT <BR>MOD_IN_USE <BR><BR>MOD_INC_USE_COUNT 是用来增加 module 的 use count，而 MOD_DEC_USE_COUNT 是用来减少 module 的 use count。至于 MOD_IN_USE 则是用来检查目前这个 module 是不是被使用中。也就是检查 use count 是否为 0。module 的 use count 必须由写 module 的人自己来 maintain。系统并不会自动为你把 use count 加一或减一。一切都得由自己控制。下面有一个例子，但是，并不会介绍这三个 macro 的使用方法。将来如果有机会，我再来介绍这三个 macro 的用法。 <BR><BR>这个例子很简单。其实只是示范如何使用 init_module() 以及 cleanup_module() 来写一个 module。当然，这两个 function 只是构成 module 的基本条件罢了。至于 module 里要提供的功能则是看各人的需要。 <BR><BR>main.c <BR>#define MODULE <BR>#include <LINUX module.h><BR>#include <ASM uaccess.h><BR>int full; <BR>EXPORT_SYMBOL(full); /* 将 full export 出去 */ <BR>int init_module( void ) <BR>{ <BR>printk( \"&lt;5&gt; Module is loaded\\n\" ); <BR>return 0; <BR>} <BR>void cleanup_module( void ) <BR>{ <BR>printk( \"&lt;5&gt; Module is unloaded\\n\" ); <BR>} <BR><BR>关于 printk 是这样子的，它是 kernel 所提供的一个打印讯息的 function。kernel 有 export 这个 function。所以你可以自由的使用它。它的用法跟 printf 几乎一模一样。唯独讯息的开头是 &lt;5&gt;，其实，不见得这三个字符啦。也可以是 &lt;4&gt;，&lt;3&gt;，&lt;7&gt; 等等的东西。这是代表这个讯息的 prioirty 或 level。&lt;5&gt; 表示的是跟 KERNEL 有关的讯息。 <BR><BR>main.ver: <BR><BR>利用 genksyms 产生出来的。 <BR><BR>gcc -E -D__GENKSYMS__ main.c | genksyms -k 2.2.1 &gt; main.ver <BR><BR>接下来，就是要把 main.c compile 成 main.o <BR><BR>gcc -D__KERNEL__ -DMODVERSIONS -DEXPORT_SYMTAB -c \\ <BR>-I/usr/src/linux/include/linux -include \\ <BR>/usr/src/linux/include/linux/modversions.h \\ <BR>-include ./main.ver main.c <BR><BR>好了。main.o 已经成功的 compile 出来了，现在下一个 command， <BR><BR>insmod main.o <BR><BR>检查看 /proc/modules 里是否有 main 这个 module。如果有，表示 main 这个 module 已经载到 kernel 了。再下一个指令，看看 full export 出去的结果。 <BR><BR>ksyms <BR><BR>结果显示 <BR><BR>Address Symbol Defined by <BR>c40220e0 full_R355b84b2 [main] <BR>c401d04c ne_probe [ne] <BR>c401a04c ei_open [8390] <BR>c401a094 ei_close [8390] <BR>c401a504 ei_interrupt [8390] <BR>c401af1c ethdev_init [8390] <BR>c401af80 NS8390_init [8390] <BR><BR>可以看到 full_R355b84b2，表示，我们已经成功的将 full 的名字加上 kernel version 的 information 了。当我们不需要这个 module 时，我们就可以下一个 command， <BR><BR>rmmod main <BR><BR>这样 main 就会被移除掉了。再检查看看 /proc/modules 就可以发现 main 那一行不见了。各位现在可以看一下 /var/log/message 这个档案，应该可以发现以两行 <BR><BR>Apr 12 14:19:05 host kernel: Module is loaded <BR>Apr 12 14:39:29 host kernel: Module is unloaded <BR><BR>这两行就是 printk 印出来的。 <BR><BR>关于 module 的介绍已经到此告一段落了。其实，使用 module 实在是很简单的一件事。对于要发展 driver 或是增加 kernel 某些新功能的人来讲，用 module 不啻为一个方便的方式。希望这篇文章对各位能有所帮助。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/503.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 01:01 <a href="http://www.cnitblog.com/drizztzou/articles/503.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux下的信号事件</title><link>http://www.cnitblog.com/drizztzou/articles/499.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:57:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/499.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/499.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/499.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/499.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/499.html</trackback:ping><description><![CDATA[前言:这一章我们讨论一下Linux下的信号处理函数. <BR>Linux下的信号处理函数: <BR>信号的产生 <BR>信号的处理 <BR>其它信号函数 <BR>一个实例 <BR>1。信号的产生 <BR>Linux下的信号可以类比于DOS下的INT或者是Windows下的事件.在有一个信号发生时候相信的信号就会发送给相应的进程.在Linux下的信号有以下几个. 我们使用 kill -l 命令可以得到以下的输出结果: <BR><BR>1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL <BR>5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE <BR>9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 <BR>13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD <BR>18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN <BR>22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ <BR>26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO <BR>30) SIGPWR <BR><BR>关于这些信号的详细解释请查看man 7 signal的输出结果. 信号事件的发生有两个来源:一个是硬件的原因(比如我们按下了键盘),一个是软件的原因(比如我们使用系统函数或者是命令发出信号). 最常用的四个发出信号的系统函数是kill, raise, alarm和setitimer函数. setitimer函数我们在计时器的使用 那一章再学习. <BR>#include <SYS types.h><BR>#include <SIGNAL.H><BR>#include <UNISTD.H><BR><BR>int kill(pid_t pid,int sig); <BR>int raise(int sig); <BR>unisigned int alarm(unsigned int seconds); <BR><BR>kill系统调用负责向进程发送信号sig. <BR>如果pid是正数,那么向信号sig被发送到进程pid. <BR>如果pid等于0,那么信号sig被发送到所以和pid进程在同一个进程组的进程 <BR>如果pid等于-1,那么信号发给所有的进程表中的进程,除了最大的哪个进程号. <BR>如果pid由于-1,和0一样,只是发送进程组是-pid. <BR>我们用最多的是第一个情况.还记得我们在守护进程那一节的例子吗?我们那个时候用这个函数杀死了父进程守护进程的创建 <BR>raise系统调用向自己发送一个sig信号.我们可以用上面那个函数来实现这个功能的. <BR>alarm函数和时间有点关系了,这个函数可以在seconds秒后向自己发送一个SIGALRM信号. 下面这个函数会有什么结果呢? <BR><BR>#include <UNISTD.H><BR><BR>main() <BR>{ <BR>unsigned int i; <BR>alarm(1); <BR>for(i=0;1;i++) <BR>printf(\"I=%d\",i); <BR>} <BR>SIGALRM的缺省操作是结束进程,所以程序在1秒之后结束,你可以看看你的最后I值为多少,来比较一下大家的系统性能差异(我的是2232). <BR><BR>2。信号操作 有时候我们希望进程正确的执行,而不想进程受到信号的影响,比如我们希望上面那个程序在1秒钟之后不结束.这个时候我们就要进行信号的操作了. <BR>信号操作最常用的方法是信号屏蔽.信号屏蔽要用到下面的几个函数. <BR><BR>#include <SIGNAL.H><BR><BR>int sigemptyset(sigset_t *set); <BR>int sigfillset(sigset_t *set); <BR>int sigaddset(sigset_t *set,int signo); <BR>int sigdelset(sigset_t *set,int signo); <BR>int sigismember(sigset_t *set,int signo); <BR>int sigprocmask(int how,const sigset_t *set,sigset_t *oset); <BR><BR>sigemptyset函数初始化信号集合set,将set设置为空.sigfillset也初始化信号集合,只是将信号集合设置为所有信号的集合.sigaddset将信号signo加入到信号集合之中,sigdelset将信号从信号集合中删除.sigismember查询信号是否在信号集合之中. <BR>sigprocmask是最为关键的一个函数.在使用之前要先设置好信号集合set.这个函数的作用是将指定的信号集合set加入到进程的信号阻塞集合之中去,如果提供了oset那么当前的进程信号阻塞集合将会保存在oset里面.参数how决定函数的操作方式. <BR>SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中. <BR>SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合. <BR>SIG_SETMASK:将当前的信号集合设置为信号阻塞集合. <BR>以一个实例来解释使用这几个函数. <BR><BR>#include <SIGNAL.H><BR>#include <STDIO.H><BR>#include <MATH.H><BR>#include <STDLIB.H><BR><BR>int main(int argc,char **argv) <BR>{ <BR>double y; <BR>sigset_t intmask; <BR>int i,repeat_factor; <BR><BR>if(argc!=2) <BR>{ <BR>fprintf(stderr,\"Usage:%s repeat_factor\\n\\a\",argv[0]); <BR>exit(1); <BR>} <BR><BR>if((repeat_factor=atoi(argv[1]))&lt;1)repeat_factor=10; <BR>sigemptyset(&amp;intmask); /* 将信号集合设置为空 */ <BR>sigaddset(&amp;intmask,SIGINT); /* 加入中断 Ctrl+C 信号*/ <BR>while(1) <BR>{ <BR>/*阻塞信号,我们不希望保存原来的集合所以参数为NULL*/ <BR>sigprocmask(SIG_BLOCK,&amp;intmask,NULL); <BR>fprintf(stderr,\"SIGINT signal blocked\\n\"); <BR>for(i=0;i<REPEAT_FACTOR;I++)Y=SIN((DOUBLE)I); <br> fprintf(stderr,\"Blocked calculation is finished\\n\"); <BR>/* 取消阻塞 */ <BR>sigprocmask(SIG_UNBLOCK,&amp;intmask,NULL); <BR>fprintf(stderr,\"SIGINT signal unblocked\\n\"); <BR>for(i=0;i<REPEAT_FACTOR;I++)Y=SIN((DOUBLE)I); <br> fprintf(stderr,\"Unblocked calculation is finished\\n\"); <BR>} <BR>exit(0); <BR>} <BR><BR>程序在运行的时候我们要使用Ctrl+C来结束.如果我们在第一计算的时候发出SIGINT信号,由于信号已经屏蔽了,所以程序没有反映.只有到信号被取消阻塞的时候程序才会结束. 注意我们只要发出一次SIGINT信号就可以了,因为信号屏蔽只是将信号加入到信号阻塞集合之中,并没有丢弃这个信号.一旦信号屏蔽取消了,这个信号就会发生作用. <BR>有时候我们希望对信号作出及时的反映的,比如当拥护按下Ctrl+C时,我们不想什么事情也不做,我们想告诉用户你的这个操作不好,请不要重试,而不是什么反映也没有的. 这个时候我们要用到sigaction函数. <BR>#include <SIGNAL.H><BR><BR>int sigaction(int signo,const struct sigaction *act, <BR>struct sigaction *oact); <BR><BR>struct sigaction { <BR>void (*sa_handler)(int signo); <BR>void (*sa_sigaction)(int siginfo_t *info,void *act); <BR>sigset_t sa_mask; <BR>int sa_flags; <BR>void (*sa_restore)(void); <BR>} <BR><BR>这个函数和结构看起来是不是有点恐怖呢.不要被这个吓着了,其实这个函数的使用相当简单的.我们先解释一下各个参数的含义. signo很简单就是我们要处理的信号了,可以是任何的合法的信号.有两个信号不能够使用(SIGKILL和SIGSTOP). act包含我们要对这个信号进行如何处理的信息.oact更简单了就是以前对这个函数的处理信息了,主要用来保存信息的,一般用NULL就OK了. <BR>信号结构有点复杂.不要紧我们慢慢的学习. <BR>sa_handler是一个函数型指针,这个指针指向一个函数,这个函数有一个参数.这个函数就是我们要进行的信号操作的函数. sa_sigaction,sa_restore和sa_handler差不多的,只是参数不同罢了.这两个元素我们很少使用,就不管了. <BR>sa_flags用来设置信号操作的各个情况.一般设置为0好了.sa_mask我们已经学习过了 <BR>在使用的时候我们用sa_handler指向我们的一个信号操作函数,就可以了.sa_handler有两个特殊的值:SIG_DEL和SIG_IGN.SIG_DEL是使用缺省的信号操作函数,而SIG_IGN是使用忽略该信号的操作函数. <BR>这个函数复杂,我们使用一个实例来说明.下面这个函数可以捕捉用户的CTRL+C信号.并输出一个提示语句. <BR><BR>#include <SIGNAL.H><BR>#include <STDIO.H><BR>#include <STRING.H><BR>#include <ERRNO.H><BR>#include <UNISTD.H><BR><BR>#define PROMPT \"你想终止程序吗?\" <BR><BR>char *prompt=PROMPT; <BR><BR>void ctrl_c_op(int signo) <BR>{ <BR>write(STDERR_FILENO,prompt,strlen(prompt)); <BR>} <BR><BR>int main() <BR>{ <BR>struct sigaction act; <BR><BR>act.sa_handler=ctrl_c_op; <BR>sigemptyset(&amp;act.sa_mask); <BR>act.sa_flags=0; <BR>if(sigaction(SIGINT,&amp;act,NULL)&lt;0) <BR>{ <BR>fprintf(stderr,\"Install Signal Action Error:%s\\n\\a\",strerror(errno)); <BR>exit(1); <BR>} <BR>while(1); <BR>} <BR><BR>在上面程序的信号操作函数之中,我们使用了write函数而没有使用fprintf函数.是因为我们要考虑到下面这种情况.如果我们在信号操作的时候又有一个信号发生,那么程序该如何运行呢? 为了处理在信号处理函数运行的时候信号的发生,我们需要设置sa_mask成员. 我们将我们要屏蔽的信号添加到sa_mask结构当中去,这样这些函数在信号处理的时候就会被屏蔽掉的. <BR>3。其它信号函数 由于信号的操作和处理比较复杂,我们再介绍几个信号操作函数. <BR><BR>#include <UNISTD.H><BR>#include <SIGNAL.H><BR><BR>int pause(void); <BR>int sigsuspend(const sigset_t *sigmask); <BR><BR>pause函数很简单,就是挂起进程直到一个信号发生了.而sigsuspend也是挂起进程只是在调用的时候用sigmask取代当前的信号阻塞集合. <BR>#include <SIGSETJMP><BR><BR>int sigsetjmp(sigjmp_buf env,int val); <BR>void siglongjmp(sigjmp_buf env,int val); <BR><BR>还记得goto函数或者是setjmp和longjmp函数吗.这两个信号跳转函数也可以实现程序的跳转让我们可以从函数之中跳转到我们需要的地方. <BR>由于上面几个函数,我们很少遇到,所以只是说明了一下,详细情况请查看联机帮助. <BR>4。一个实例 还记得我们在守护进程创建的哪个程序吗?守护进程在这里我们把那个程序加强一下. 下面这个程序会在也可以检查用户的邮件.不过提供了一个开关,如果用户不想程序提示有新的邮件到来,可以向程序发送SIGUSR2信号,如果想程序提供提示可以发送SIGUSR1信号. <BR><BR><BR>#include <UNISTD.H><BR>#include <STDIO.H><BR>#include <ERRNO.H><BR>#include <FCNTL.H><BR>#include <SIGNAL.H><BR>#include <STRING.H><BR>#include <PWD.H><BR><BR>#include <SYS types.h><BR>#include <SYS stat.h><BR><BR>/* Linux 的默任个人的邮箱地址是 /var/spool/mail/ */ <BR><BR>#define MAIL_DIR \"/var/spool/mail/\" <BR><BR>/* 睡眠10秒钟 */ <BR><BR>#define SLEEP_TIME 10 <BR>#define MAX_FILENAME 255 <BR><BR>unsigned char notifyflag=1; <BR><BR>long get_file_size(const char *filename) <BR>{ <BR>struct stat buf; <BR><BR>if(stat(filename,&amp;;buf)==-1) <BR>{ <BR>if(errno==ENOENT)return 0; <BR>else return -1; <BR>} <BR>return (long)buf.st_size; <BR>} <BR><BR>void send_mail_notify(void) <BR>{ <BR>fprintf(stderr,\"New mail has arrived\\007\\n\"); <BR>} <BR><BR>void turn_on_notify(int signo) <BR>{ <BR>notifyflag=1; <BR>} <BR><BR>void turn_off_notify(int signo) <BR>{ <BR>notifyflag=0; <BR>} <BR><BR>int check_mail(const char *filename) <BR>{ <BR>long old_mail_size,new_mail_size; <BR>sigset_t blockset,emptyset; <BR><BR>sigemptyset(&amp;;blockset); <BR>sigemptyset(&amp;;emptyset); <BR>sigaddset(&amp;;blockset,SIGUSR1); <BR>sigaddset(&amp;;blockset,SIGUSR2); <BR><BR>old_mail_size=get_file_size(filename); <BR>if(old_mail_size&lt;0)return 1; <BR>if(old_mail_size&gt;0) send_mail_notify(); <BR>sleep(SLEEP_TIME); <BR><BR>while(1) <BR>{ <BR>if(sigprocmask(SIG_BLOCK,&amp;;blockset,NULL)&lt;0) return 1; <BR>while(notifyflag==0)sigsuspend(&amp;;emptyset); <BR>if(sigprocmask(SIG_SETMASK,&amp;;emptyset,NULL)&lt;0) return 1; <BR>new_mail_size=get_file_size(filename); <BR>if(new_mail_size&gt;old_mail_size)send_mail_notify; <BR>old_mail_size=new_mail_size; <BR>sleep(SLEEP_TIME); <BR>} <BR>} <BR><BR>int main(void) <BR>{ <BR>char mailfile[MAX_FILENAME]; <BR>struct sigaction newact; <BR>struct passwd *pw; <BR><BR>if((pw=getpwuid(getuid()))==NULL) <BR>{ <BR>fprintf(stderr,\"Get Login Name Error:%s\\n\\a\",strerror(errno)); <BR>exit(1); <BR>} <BR>strcpy(mailfile,MAIL_DIR); <BR>strcat(mailfile,pw-&gt;pw_name); <BR>newact.sa_handler=turn_on_notify; <BR>newact.sa_flags=0; <BR>sigemptyset(&amp;;newact.sa_mask); <BR>sigaddset(&amp;;newact.sa_mask,SIGUSR1); <BR>sigaddset(&amp;;newact.sa_mask,SIGUSR2); <BR>if(sigaction(SIGUSR1,&amp;;newact,NULL)&lt;0) <BR>fprintf(stderr,\"Turn On Error:%s\\n\\a\",strerror(errno)); <BR>newact.sa_handler=turn_off_notify; <BR>if(sigaction(SIGUSR1,&amp;;newact,NULL)&lt;0) <BR>fprintf(stderr,\"Turn Off Error:%s\\n\\a\",strerror(errno)); <BR>check_mail(mailfile); <BR>exit(0); <BR>} <BR><BR>信号操作是一件非常复杂的事情,比我们想象之中的复杂程度还要复杂,如果你想彻底的弄清楚信号操作的各个问题,那么除了大量的练习以外还要多看联机手册.不过如果我们只是一般的使用的话,有了上面的几个函数也就差不多了. 我们就介绍到这里了.<img src ="http://www.cnitblog.com/drizztzou/aggbug/499.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:57 <a href="http://www.cnitblog.com/drizztzou/articles/499.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux下文件的操作</title><link>http://www.cnitblog.com/drizztzou/articles/500.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:57:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/500.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/500.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/500.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/500.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/500.html</trackback:ping><description><![CDATA[前言: <BR>我们在这一节将要讨论linux下文件操作的各个函数. <BR>文件的创建和读写 <BR>文件的各个属性 <BR>目录文件的操作 <BR>管道文件 <BR><BR>---------------------------------------------------------------- <BR>1。文件的创建和读写 <BR>我假设你已经知道了标准级的文件操作的各个函数(fopen,fread,fwrite等等).当然如果你不清楚的话也不要着急.我们讨论的系统级的文件操作实际上是为标准级文件操作服务的. <BR>当我们需要打开一个文件进行读写操作的时候,我们可以使用系统调用函数open.使用完成以后我们调用另外一个close函数进行关闭操作. <BR>#include <FCNTL.H><BR>#include <UNISTD.H><BR>#include <SYS types.h><BR>#include <SYS stat.h><BR><BR>int open(const char *pathname,int flags); <BR>int open(const char *pathname,int flags,mode_t mode); <BR><BR>int close(int fd); <BR><BR>open函数有两个形式.其中pathname是我们要打开的文件名(包含路径名称,缺省是认为在当前路径下面).flags可以去下面的一个值或者是几个值的组合. <BR>O_RDONLY:以只读的方式打开文件. <BR>O_WRONLY:以只写的方式打开文件. <BR>O_RDWR:以读写的方式打开文件. <BR>O_APPEND:以追加的方式打开文件. <BR>O_CREAT:创建一个文件. <BR>O_EXEC:如果使用了O_CREAT而且文件已经存在,就会发生一个错误. <BR>O_NOBLOCK:以非阻塞的方式打开一个文件. <BR>O_TRUNC:如果文件已经存在,则删除文件的内容. <BR>前面三个标志只能使用任意的一个.如果使用了O_CREATE标志,那么我们要使用open的第二种形式.还要指定mode标志,用来表示文件的访问权限.mode可以是以下情况的组合. <BR>----------------------------------------------------------------- <BR>S_IRUSR 用户可以读 S_IWUSR 用户可以写 <BR>S_IXUSR 用户可以执行 S_IRWXU 用户可以读写执行 <BR>----------------------------------------------------------------- <BR>S_IRGRP 组可以读 S_IWGRP 组可以写 <BR>S_IXGRP 组可以执行 S_IRWXG 组可以读写执行 <BR>----------------------------------------------------------------- <BR>S_IROTH 其他人可以读 S_IWOTH 其他人可以写 <BR>S_IXOTH 其他人可以执行 S_IRWXO 其他人可以读写执行 <BR>----------------------------------------------------------------- <BR>S_ISUID 设置用户执行ID S_ISGID 设置组的执行ID <BR>----------------------------------------------------------------- <BR>我们也可以用数字来代表各个位的标志.Linux总共用5个数字来表示文件的各种权限. <BR>00000.第一位表示设置用户ID.第二位表示设置组ID,第三位表示用户自己的权限位,第四位表示组的权限,最后一位表示其他人的权限. <BR>每个数字可以取1(执行权限),2(写权限),4(读权限),0(什么也没有)或者是这几个值的和. <BR>比如我们要创建一个用户读写执行,组没有权限,其他人读执行的文件.设置用户ID位那么我们可以使用的模式是--1(设置用户ID)0(组没有设置)7(1+2+4)0(没有权限,使用缺省)5(1+4)即10705: <BR>open(\"temp\",O_CREAT,10705); <BR>如果我们打开文件成功,open会返回一个文件描述符.我们以后对文件的所有操作就可以对这个文件描述符进行操作了. <BR>当我们操作完成以后,我们要关闭文件了,只要调用close就可以了,其中fd是我们要关闭的文件描述符. <BR>文件打开了以后,我们就要对文件进行读写了.我们可以调用函数read和write进行文件的读写. <BR>#include <UNISTD.H><BR><BR>ssize_t read(int fd, void *buffer,size_t count); <BR>ssize_t write(int fd, const void *buffer,size_t count); <BR><BR>fd是我们要进行读写操作的文件描述符,buffer是我们要写入文件内容或读出文件内容的内存地址.count是我们要读写的字节数. <BR>对于普通的文件read从指定的文件(fd)中读取count字节到buffer缓冲区中(记住我们必须提供一个足够大的缓冲区),同时返回count. <BR>如果read读到了文件的结尾或者被一个信号所中断,返回值会小于count.如果是由信号中断引起返回,而且没有返回数据,read会返回-1,且设置errno为EINTR.当程序读到了文件结尾的时候,read会返回0. <BR>write从buffer中写count字节到文件fd中,成功时返回实际所写的字节数. <BR>下面我们学习一个实例,这个实例用来拷贝文件. <BR><BR>#include <UNISTD.H><BR>#include <FCNTL.H><BR>#include <STDIO.H><BR>#include <SYS types.h><BR>#include <SYS stat.h><BR>#include <ERRNO.H><BR>#include <STRING.H><BR><BR>#define BUFFER_SIZE 1024 <BR><BR>int main(int argc,char **argv) <BR>{ <BR><BR>int from_fd,to_fd; <BR>int bytes_read,bytes_write; <BR>char buffer[BUFFER_SIZE]; <BR>char *ptr; <BR><BR>if(argc!=3) <BR>{ <BR>fprintf(stderr,\"Usage:%s fromfile tofile\\n\\a\",argv[0]); <BR>exit(1); <BR>} <BR><BR>/* 打开源文件 */ <BR><BR>if((from_fd=open(argv[1],O_RDONLY))==-1) <BR>{ <BR>fprintf(stderr,\"Open %s Error:%s\\n\",argv[1],strerror(errno)); <BR>exit(1); <BR>} <BR><BR>/* 创建目的文件 */ <BR><BR>if((to_fd=open(argv[2],O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR))==-1) <BR>{ <BR>fprintf(stderr,\"Open %s Error:%s\\n\",argv[2],strerror(errno)); <BR>exit(1); <BR>} <BR><BR>/* 以下代码是一个经典的拷贝文件的代码 */ <BR><BR>while(bytes_read=read(from_fd,buffer,BUFFER_SIZE)) <BR>{ <BR>/* 一个致命的错误发生了 */ <BR>if((bytes_read==-1)&amp;&amp;(errno!=EINTR)) break; <BR>else if(bytes_read&gt;0) <BR>{ <BR>ptr=buffer; <BR>while(bytes_write=write(to_fd,ptr,bytes_read)) <BR>{ <BR>/* 一个致命错误发生了 */ <BR>if((bytes_write==-1)&amp;&amp;(errno!=EINTR))break; <BR>/* 写完了所有读的字节 */ <BR>else if(bytes_write==bytes_read) break; <BR>/* 只写了一部分,继续写 */ <BR>else if(bytes_write&gt;0) <BR>{ <BR>ptr+=bytes_write; <BR>bytes_read-=bytes_write; <BR>} <BR>} <BR>/* 写的时候发生的致命错误 */ <BR>if(bytes_write==-1)break; <BR><BR>} <BR>} <BR>close(from_fd); <BR>close(to_fd); <BR>exit(0); <BR>} <BR><BR>2。文件的各个属性 <BR>文件具有各种各样的属性,除了我们上面所知道的文件权限以外,文件还有创建时间,大小等等属性. <BR>有时侯我们要判断文件是否可以进行某种操作(读,写等等).这个时候我们可以使用access函数. <BR>#include <UNISTD.H><BR><BR>int access(const char *pathname,int mode); <BR><BR>pathname:是文件名称,mode是我们要判断的属性.可以取以下值或者是他们的组合. <BR>R_OK文件可以读,W_OK文件可以写,X_OK文件可以执行,F_OK文件存在.当我们测试成功时,函数返回0,否则如果有一个条件不符时,返回-1. <BR>如果我们要获得文件的其他属性,我们可以使用函数stat或者fstat. <BR>#include <SYS stat.h><BR>#include <UNISTD.H><BR><BR>int stat(const char *file_name,struct stat *buf); <BR>int fstat(int filedes,struct stat *buf); <BR><BR>struct stat { <BR>dev_t st_dev; /* 设备 */ <BR>ino_t st_ino; /* 节点 */ <BR>mode_t st_mode; /* 模式 */ <BR>nlink_t st_nlink; /* 硬连接 */ <BR>uid_t st_uid; /* 用户ID */ <BR>gid_t st_gid; /* 组ID */ <BR>dev_t st_rdev; /* 设备类型 */ <BR>off_t st_off; /* 文件字节数 */ <BR>unsigned long st_blksize; /* 块大小 */ <BR>unsigned long st_blocks; /* 块数 */ <BR>time_t st_atime; /* 最后一次访问时间 */ <BR>time_t st_mtime; /* 最后一次修改时间 */ <BR>time_t st_ctime; /* 最后一次改变时间(指属性) */ <BR>}; <BR><BR>stat用来判断没有打开的文件,而fstat用来判断打开的文件.我们使用最多的属性是st_mode.通过着属性我们可以判断给定的文件是一个普通文件还是一个目录,连接等等.可以使用下面几个宏来判断. <BR>S_ISLNK(st_mode):是否是一个连接.S_ISREG是否是一个常规文件.S_ISDIR是否是一个目录S_ISCHR是否是一个字符设备.S_ISBLK是否是一个块设备S_ISFIFO是否 是一个FIFO文件.S_ISSOCK是否是一个SOCKET文件. 我们会在下面说明如何使用这几个宏的. <BR>3。目录文件的操作 <BR>在我们编写程序的时候，有时候会要得到我们当前的工作路径。C库函数提供了getcwd来解决这个问题。 <BR>#include <UNISTD.H><BR><BR>char *getcwd(char *buffer,size_t size); <BR><BR>我们提供一个size大小的buffer,getcwd会把我们当前的路径考到buffer中.如果buffer太小,函数会返回-1和一个错误号. <BR>Linux提供了大量的目录操作函数,我们学习几个比较简单和常用的函数. <BR>#include <DIRENT.H><BR>#include <UNISTD.H><BR>#include <FCNTL.H><BR>#include <SYS types.h><BR>#include <SYS stat.h><BR><BR>int mkdir(const char *path,mode_t mode); <BR>DIR *opendir(const char *path); <BR>struct dirent *readdir(DIR *dir); <BR>void rewinddir(DIR *dir); <BR>off_t telldir(DIR *dir); <BR>void seekdir(DIR *dir,off_t off); <BR>int closedir(DIR *dir); <BR><BR>struct dirent { <BR>long d_ino; <BR>off_t d_off; <BR>unsigned short d_reclen; <BR>char d_name[NAME_MAX+1]; /* 文件名称 */ <BR><BR>mkdir很容易就是我们创建一个目录,opendir打开一个目录为以后读做准备.readdir读一个打开的目录.rewinddir是用来重读目录的和我们学的rewind函数一样.closedir是关闭一个目录.telldir和seekdir类似与ftee和fseek函数. <BR>下面我们开发一个小程序,这个程序有一个参数.如果这个参数是一个文件名,我们输出这个文件的大小和最后修改的时间,如果是一个目录我们输出这个目录下所有文件的大小和修改时间. <BR><BR>#include <UNISTD.H><BR>#include <STDIO.H><BR>#include <ERRNO.H><BR>#include <SYS types.h><BR>#include <SYS stat.h><BR>#include <DIRENT.H><BR>#include <TIME.H><BR><BR>static int get_file_size_time(const char *filename) <BR>{ <BR>struct stat statbuf; <BR><BR>if(stat(filename,&amp;statbuf)==-1) <BR>{ <BR>printf(\"Get stat on %s Error:%s\\n\", <BR>filename,strerror(errno)); <BR>return(-1); <BR>} <BR><BR>if(S_ISDIR(statbuf.st_mode))return(1); <BR>if(S_ISREG(statbuf.st_mode)) <BR>printf(\"%s size:%ld bytes\\tmodified at %s\", <BR>filename,statbuf.st_size,ctime(&amp;statbuf.st_mtime)); <BR><BR>return(0); <BR>} <BR><BR>int main(int argc,char **argv) <BR>{ <BR>DIR *dirp; <BR>struct dirent *direntp; <BR>int stats; <BR><BR>if(argc!=2) <BR>{ <BR>printf(\"Usage:%s filename\\n\\a\",argv[0]); <BR>exit(1); <BR>} <BR><BR>if(((stats=get_file_size_time(argv[1]))==0)||(stats==-1))exit(1); <BR><BR>if((dirp=opendir(argv[1]))==NULL) <BR>{ <BR>printf(\"Open Directory %s Error:%s\\n\", <BR>argv[1],strerror(errno)); <BR>exit(1); <BR>} <BR><BR>while((direntp=readdir(dirp))!=NULL) <BR>if(get_file_size_time(direntp-<D_NAME)==-1)BREAK; <br> closedir(dirp); <BR>exit(1); <BR>} <BR><BR>4。管道文件 <BR>Linux提供了许多的过滤和重定向程序,比如more cat <BR>等等.还提供了&lt; &gt; | &lt;&lt;等等重定向操作符.在这些过滤和重 定向程序当中,都用到了管道这种特殊的文件.系统调用pipe可以创建一个管道. <BR>#include<UNISTD.H> <BR><BR>int pipe(int fildes[2]); <BR><BR>pipe调用可以创建一个管道(通信缓冲区).当调用成功时,我们可以访问文件描述符fildes[0],fildes[1].其中fildes[0]是用来读的文件描述符,而fildes[1]是用来写的文件描述符. <BR>在实际使用中我们是通过创建一个子进程,然后一个进程写,一个进程读来使用的. <BR>关于进程通信的详细情况请查看进程通信 <BR><BR>#include <STDIO.H><BR>#include <STDLIB.H><BR>#include <UNISTD.H><BR>#include <STRING.H><BR>#include <ERRNO.H><BR>#include <SYS types.h><BR>#include <SYS wait.h><BR>#define BUFFER 255 <BR><BR>int main(int argc,char **argv) <BR>{ <BR>char buffer[BUFFER+1]; <BR>int fd[2]; <BR><BR>if(argc!=2) <BR>{ <BR>fprintf(stderr,\"Usage:%s string\\n\\a\",argv[0]); <BR>exit(1); <BR>} <BR><BR>if(pipe(fd)!=0) <BR>{ <BR>fprintf(stderr,\"Pipe Error:%s\\n\\a\",strerror(errno)); <BR>exit(1); <BR>} <BR>if(fork()==0) <BR>{ <BR>close(fd[0]); <BR>printf(\"Child[%d] Write to pipe\\n\\a\",getpid()); <BR>snprintf(buffer,BUFFER,\"%s\",argv[1]); <BR>write(fd[1],buffer,strlen(buffer)); <BR>printf(\"Child[%d] Quit\\n\\a\",getpid()); <BR>exit(0); <BR>} <BR>else <BR>{ <BR>close(fd[1]); <BR>printf(\"Parent[%d] Read from pipe\\n\\a\",getpid()); <BR>memset(buffer,\\\0\,BUFFER+1); <BR>read(fd[0],buffer,BUFFER); <BR>printf(\"Parent[%d] Read:%s\\n\",getpid(),buffer); <BR>exit(1); <BR>} <BR>} <BR><BR>为了实现重定向操作,我们需要调用另外一个函数dup2. <BR>#include <UNISTD.H><BR><BR>int dup2(int oldfd,int newfd); <BR><BR>dup2将用oldfd文件描述符来代替newfd文件描述符,同时关闭newfd文件描述符.也就是说, <BR>所有向newfd操作都转到oldfd上面.下面我们学习一个例子,这个例子将标准输出重定向到一个文件. <BR><BR>#include <UNISTD.H><BR>#include <STDIO.H><BR>#include <ERRNO.H><BR>#include <FCNTL.H><BR>#include <STRING.H><BR>#include <SYS types.h><BR>#include <SYS stat.h><BR><BR>#define BUFFER_SIZE 1024 <BR><BR>int main(int argc,char **argv) <BR>{ <BR>int fd; <BR>char buffer[BUFFER_SIZE]; <BR><BR>if(argc!=2) <BR>{ <BR>fprintf(stderr,\"Usage:%s outfilename\\n\\a\",argv[0]); <BR>exit(1); <BR>} <BR><BR>if((fd=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR))==-1) <BR>{ <BR>fprintf(stderr,\"Open %s Error:%s\\n\\a\",argv[1],strerror(errno)); <BR>exit(1); <BR>} <BR><BR>if(dup2(fd,STDOUT_FILENO)==-1) <BR>{ <BR>fprintf(stderr,\"Redirect Standard Out Error:%s\\n\\a\",strerror(errno)); <BR>exit(1); <BR>} <BR><BR>fprintf(stderr,\"Now,please input string\"); <BR>fprintf(stderr,\"(To quit use CTRL+D)\\n\"); <BR>while(1) <BR>{ <BR>fgets(buffer,BUFFER_SIZE,stdin); <BR>if(feof(stdin))break; <BR>write(STDOUT_FILENO,buffer,strlen(buffer)); <BR>} <BR>exit(0); <BR>} <BR><BR>好了,文件一章我们就暂时先讨论到这里,学习好了文件的操作我们其实已经可以写出一些比较有用的程序了.我们可以编写一个实现例如dir,mkdir,cp,mv等等常用的文件操作命令了. <BR>想不想自己写几个试一试呢? <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/500.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:57 <a href="http://www.cnitblog.com/drizztzou/articles/500.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux下的进程通信</title><link>http://www.cnitblog.com/drizztzou/articles/498.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:56:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/498.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/498.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/498.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/498.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/498.html</trackback:ping><description><![CDATA[前言:Linux下的进程通信(IPC) <BR>Linux下的进程通信(IPC) <BR>POSIX无名信号量 <BR>System V信号量 <BR>System V消息队列 <BR>System V共享内存 <BR>1。POSIX无名信号量 如果你学习过操作系统,那么肯定熟悉PV操作了.PV操作是原子操作.也就是操作是不可以中断的,在一定的时间内,只能够有一个进程的代码在CPU上面执行.在系统当中,有时候为了顺利的使用和保护共享资源,大家提出了信号的概念. 假设我们要使用一台打印机,如果在同一时刻有两个进程在向打印机输出,那么最终的结果会是什么呢.为了处理这种情况,POSIX标准提出了有名信号量和无名信号量的概念,由于Linux只实现了无名信号量,我们在这里就只是介绍无名信号量了. 信号量的使用主要是用来保护共享资源,使的资源在一个时刻只有一个进程所拥有.为此我们可以使用一个信号灯.当信号灯的值为某个值的时候,就表明此时资源不可以使用.否则就表&gt;示可以使用. 为了提供效率,系统提供了下面几个函数 <BR>POSIX的无名信号量的函数有以下几个: <BR><BR>#include <SEMAPHORE.H><BR><BR>int sem_init(sem_t *sem,int pshared,unsigned int value); <BR>int sem_destroy(sem_t *sem); <BR>int sem_wait(sem_t *sem); <BR>int sem_trywait(sem_t *sem); <BR>int sem_post(sem_t *sem); <BR>int sem_getvalue(sem_t *sem); <BR><BR>sem_init创建一个信号灯,并初始化其值为value.pshared决定了信号量能否在几个进程间共享.由于目前Linux还没有实现进程间共享信号灯,所以这个值只能够取0. sem_destroy是用来删除信号灯的.sem_wait调用将阻塞进程,直到信号灯的值大于0.这个函数返回的时候自动的将信号灯的值的件一.sem_post和sem_wait相反,是将信号灯的内容加一同时发出信号唤醒等待的进程..sem_trywait和sem_wait相同,不过不阻塞的,当信号灯的值为0的时候返回EAGAIN,表示以后重试.sem_getvalue得到信号灯的值. <BR>由于Linux不支持,我们没有办法用源程序解释了. <BR>这几个函数的使用相当简单的.比如我们有一个程序要向一个系统打印机打印两页.我们首先创建一个信号灯,并使其初始值为1,表示我们有一个资源可用.然后一个进程调用sem_wait由于这个时候信号灯的值为1,所以这个函数返回,打印机开始打印了,同时信号灯的值为0 了. 如果第二个进程要打印,调用sem_wait时候,由于信号灯的值为0,资源不可用,于是被阻塞了.当第一个进程打印完成以后,调用sem_post信号灯的值为1了,这个时候系统通知第二个进程,于是第二个进程的sem_wait返回.第二个进程开始打印了. <BR>不过我们可以使用线程来解决这个问题的.我们会在后面解释什么是线程的.编译包含上面这几个函数的程序要加上 -lrt选贤,以连接librt.so库 <BR>2。System V信号量 为了解决上面哪个问题,我们也可以使用System V信号量.很幸运的是Linux实现了System V信号量.这样我们就可以用实例来解释了. System V信号量的函数主要有下面几个. <BR><BR>#include <SYS types.h><BR>#include <SYS ipc.h><BR>#include <SYS sem.h><BR><BR>key_t ftok(char *pathname,char proj); <BR>int semget(key_t key,int nsems,int semflg); <BR>int semctl(int semid,int semnum,int cmd,union semun arg); <BR>int semop(int semid,struct sembuf *spos,int nspos); <BR><BR>struct sembuf { <BR>short sem_num; /* 使用那一个信号 */ <BR>short sem_op; /* 进行什么操作 */ <BR>short sem_flg; /* 操作的标志 */ <BR>}; <BR><BR><BR>ftok函数是根据pathname和proj来创建一个关键字.semget创建一个信号量.成功时返回信号的ID,key是一个关键字,可以是用ftok创建的也可以是IPC_PRIVATE表明由系统选用一个关键字. nsems表明我们创建的信号个数.semflg是创建的权限标志,和我们创建一个文件的标志相同. <BR>semctl对信号量进行一系列的控制.semid是要操作的信号标志,semnum是信号的个数,cmd是操作的命令.经常用的两个值是:SETVAL(设置信号量的值)和IPC_RMID(删除信号灯).arg是一个给cmd的参数. <BR>semop是对信号进行操作的函数.semid是信号标志,spos是一个操作数组表明要进行什么操作,nspos表明数组的个数. 如果sem_op大于0,那么操作将sem_op加入到信号量的值中,并唤醒等待信号增加的进程. 如果为0,当信号量的值是0的时候,函数返回,否则阻塞直到信号量的值为0. 如果小于0,函数判断信号量的值加上这个负值.如果结果为0唤醒等待信号量为0的进程,如果小与0函数阻塞.如果大于0,那么从信号量里面减去这个值并返回. <BR>下面我们一以一个实例来说明这几个函数的使用方法.这个程序用标准错误输出来代替我们用的打印机. <BR><BR>#include <STDIO.H><BR>#include <UNISTD.H><BR>#include <LIMITS.H><BR>#include <ERRNO.H><BR>#include <STRING.H><BR>#include <STDLIB.H><BR>#include <SYS stat.h><BR>#include <SYS wait.h><BR>#include <SYS ipc.h><BR>#include <SYS sem.h><BR><BR>#define PERMS S_IRUSR|S_IWUSR <BR><BR>void init_semaphore_struct(struct sembuf *sem,int semnum, <BR>int semop,int semflg) <BR>{ <BR>/* 初始话信号灯结构 */ <BR>sem-&gt;sem_num=semnum; <BR>sem-&gt;sem_op=semop; <BR>sem-&gt;sem_flg=semflg; <BR>} <BR><BR>int del_semaphore(int semid) <BR>{ <BR>/* 信号灯并不随程序的结束而被删除,如果我们没删除的话(将1改为0) <BR>可以用ipcs命令查看到信号灯,用ipcrm可以删除信号灯的 <BR>*/ <BR>#if 1 <BR>return semctl(semid,0,IPC_RMID); <BR>#endif <BR>} <BR><BR>int main(int argc,char **argv) <BR>{ <BR>char buffer[MAX_CANON],*c; <BR>int i,n; <BR>int semid,semop_ret,status; <BR>pid_t childpid; <BR>struct sembuf semwait,semsignal; <BR><BR>if((argc!=2)||((n=atoi(argv[1]))&lt;1)) <BR>{ <BR>fprintf(stderr,\"Usage:%s number\\n\\a\",argv[0]); <BR>exit(1); <BR>} <BR><BR>/* 使用IPC_PRIVATE 表示由系统选择一个关键字来创建 */ <BR>/* 创建以后信号灯的初始值为0 */ <BR>if((semid=semget(IPC_PRIVATE,1,PERMS))==-1) <BR>{ <BR>fprintf(stderr,\"[%d]:Acess Semaphore Error:%s\\n\\a\", <BR>getpid(),strerror(errno)); <BR>exit(1); <BR>} <BR><BR>/* semwait是要求资源的操作(-1) */ <BR>init_semaphore_struct(&amp;semwait,0,-1,0); <BR><BR>/* semsignal是释放资源的操作(+1) */ <BR>init_semaphore_struct(&amp;semsignal,0,1,0); <BR><BR>/* 开始的时候有一个系统资源(一个标准错误输出) */ <BR>if(semop(semid,&amp;semsignal,1)==-1) <BR>{ <BR>fprintf(stderr,\"[%d]:Increment Semaphore Error:%s\\n\\a\", <BR>getpid(),strerror(errno)); <BR>if(del_semaphore(semid)==-1) <BR>fprintf(stderr,\"[%d]:Destroy Semaphore Error:%s\\n\\a\", <BR>getpid(),strerror(errno)); <BR>exit(1); <BR>} <BR><BR>/* 创建一个进程链 */ <BR>for(i=0;i<N;I++) <br> if(childpid=fork()) break; <BR><BR>sprintf(buffer,\"[i=%d]--&gt;[Process=%d]--&gt;[Parent=%d]--&gt;[Child=%d]\\n\", <BR>i,getpid(),getppid(),childpid); <BR>c=buffer; <BR><BR>/* 这里要求资源,进入原子操作 */ <BR>while(((semop_ret=semop(semid,&amp;semwait,1))==-1)&amp;&amp;(errno==EINTR)); <BR>if(semop_ret==-1) <BR>{ <BR>fprintf(stderr,\"[%d]:Decrement Semaphore Error:%s\\n\\a\", <BR>getpid(),strerror(errno)); <BR>} <BR>else <BR>{ <BR>while(*c!=\\\0\)fputc(*c++,stderr); <BR>/* 原子操作完成,赶快释放资源 */ <BR>while(((semop_ret=semop(semid,&amp;semsignal,1))==-1)&amp;&amp;(errno==EINTR)); <BR>if(semop_ret==-1) <BR>fprintf(stderr,\"[%d]:Increment Semaphore Error:%s\\n\\a\", <BR>getpid(),strerror(errno)); <BR>} <BR><BR>/* 不能够在其他进程反问信号灯的时候,我们删除了信号灯 */ <BR>while((wait(&amp;status)==-1)&amp;&amp;(errno==EINTR)); <BR>/* 信号灯只能够被删除一次的 */ <BR>if(i==1) <BR>if(del_semaphore(semid)==-1) <BR>fprintf(stderr,\"[%d]:Destroy Semaphore Error:%s\\n\\a\", <BR>getpid(),strerror(errno)); <BR>exit(0); <BR>} <BR><BR>信号灯的主要用途是保护临界资源(在一个时刻只被一个进程所拥有). <BR>3。SystemV消息队列 为了便于进程之间通信,我们可以使用管道通信 SystemV也提供了一些函数来实现进程的通信.这就是消息队列. <BR><BR>#include <SYS types.h><BR>#include <SYS ipc.h><BR>#include <SYS msg.h><BR><BR>int msgget(key_t key,int msgflg); <BR>int msgsnd(int msgid,struct msgbuf *msgp,int msgsz,int msgflg); <BR>int msgrcv(int msgid,struct msgbuf *msgp,int msgsz, <BR>long msgtype,int msgflg); <BR>int msgctl(Int msgid,int cmd,struct msqid_ds *buf); <BR><BR>struct msgbuf { <BR>long msgtype; /* 消息类型 */ <BR>....... /* 其他数据类型 */ <BR>} <BR><BR>msgget函数和semget一样,返回一个消息队列的标志.msgctl和semctl是对消息进行控制. msgsnd和msgrcv函数是用来进行消息通讯的.msgid是接受或者发送的消息队列标志. msgp是接受或者发送的内容.msgsz是消息的大小. 结构msgbuf包含的内容是至少有一个为msgtype.其他的成分是用户定义的.对于发送函数msgflg指出缓冲区用完时候的操作.接受函数指出无消息时候的处理.一般为0. 接收函数msgtype指出接收消息时候的操作. <BR>如果msgtype=0,接收消息队列的第一个消息.大于0接收队列中消息类型等于这个值的第一个消息.小于0接收消息队列中小于或者等于msgtype绝对值的所有消息中的最小一个消息. 我们以一个实例来解释进程通信.下面这个程序有server和client组成.先运行服务端后运行客户端. <BR>服务端 server.c <BR><BR>#include <STDIO.H><BR>#include <STRING.H><BR>#include <STDLIB.H><BR>#include <ERRNO.H><BR>#include <UNISTD.H><BR>#include <SYS types.h><BR>#include <SYS ipc.h><BR>#include <SYS stat.h><BR>#include <SYS msg.h><BR><BR>#define MSG_FILE \"server.c\" <BR>#define BUFFER 255 <BR>#define PERM S_IRUSR|S_IWUSR <BR><BR>struct msgtype { <BR>long mtype; <BR>char buffer[BUFFER+1]; <BR>}; <BR><BR>int main() <BR>{ <BR>struct msgtype msg; <BR>key_t key; <BR>int msgid; <BR><BR>if((key=ftok(MSG_FILE,\a\))==-1) <BR>{ <BR>fprintf(stderr,\"Creat Key Error:%s\\a\\n\",strerror(errno)); <BR>exit(1); <BR>} <BR><BR>if((msgid=msgget(key,PERM|IPC_CREAT|IPC_EXCL))==-1) <BR>{ <BR>fprintf(stderr,\"Creat Message Error:%s\\a\\n\",strerror(errno)); <BR>exit(1); <BR>} <BR><BR>while(1) <BR>{ <BR>msgrcv(msgid,&amp;msg,sizeof(struct msgtype),1,0); <BR>fprintf(stderr,\"Server Receive:%s\\n\",msg.buffer); <BR>msg.mtype=2; <BR>msgsnd(msgid,&amp;msg,sizeof(struct msgtype),0); <BR>} <BR>exit(0); <BR>} <BR><BR><BR>-------------------------------------------------------------------------------- <BR><BR>客户端(client.c) <BR><BR>#include <STDIO.H><BR>#include <STRING.H><BR>#include <STDLIB.H><BR>#include <ERRNO.H><BR>#include <SYS types.h><BR>#include <SYS ipc.h><BR>#include <SYS msg.h><BR>#include <SYS stat.h><BR>#define MSG_FILE \"server.c\" <BR>#define BUFFER 255 <BR>#define PERM S_IRUSR|S_IWUSR <BR><BR>struct msgtype { <BR>long mtype; <BR>char buffer[BUFFER+1]; <BR>}; <BR><BR>int main(int argc,char **argv) <BR>{ <BR>struct msgtype msg; <BR>key_t key; <BR>int msgid; <BR><BR>if(argc!=2) <BR>{ <BR>fprintf(stderr,\"Usage:%s string\\n\\a\",argv[0]); <BR>exit(1); <BR>} <BR><BR>if((key=ftok(MSG_FILE,\a\))==-1) <BR>{ <BR>fprintf(stderr,\"Creat Key Error:%s\\a\\n\",strerror(errno)); <BR>exit(1); <BR>} <BR><BR>if((msgid=msgget(key,PERM))==-1) <BR>{ <BR>fprintf(stderr,\"Creat Message Error:%s\\a\\n\",strerror(errno)); <BR>exit(1); <BR>} <BR><BR>msg.mtype=1; <BR>strncpy(msg.buffer,argv[1],BUFFER); <BR>msgsnd(msgid,&amp;msg,sizeof(struct msgtype),0); <BR>memset(&amp;msg,\\\0\,sizeof(struct msgtype)); <BR>msgrcv(msgid,&amp;msg,sizeof(struct msgtype),2,0); <BR>fprintf(stderr,\"Client receive:%s\\n\",msg.buffer); <BR>exit(0); <BR>} <BR><BR>注意服务端创建的消息队列最后没有删除,我们要使用ipcrm命令来删除的. <BR>4。SystemV共享内存 还有一个进程通信的方法是使用共享内存.SystemV提供了以下几个函数以实现共享内存. <BR><BR>#include <SYS types.h><BR>#include <SYS ipc.h><BR>#include <SYS shm.h><BR><BR>int shmget(key_t key,int size,int shmflg); <BR>void *shmat(int shmid,const void *shmaddr,int shmflg); <BR>int shmdt(const void *shmaddr); <BR>int shmctl(int shmid,int cmd,struct shmid_ds *buf); <BR><BR>shmget和shmctl没有什么好解释的.size是共享内存的大小. shmat是用来连接共享内存的.shmdt是用来断开共享内存的.不要被共享内存词语吓倒,共享内存其实很容易实现和使用的.shmaddr,shmflg我们只要用0代替就可以了.在使用一个共享内存之前我们调用shmat得到共享内存的开始地址,使用结束以后我们使用shmdt断开这个内存. <BR><BR>#include <STDIO.H><BR>#include <STRING.H><BR>#include <ERRNO.H><BR>#include <UNISTD.H><BR>#include <SYS stat.h><BR>#include <SYS types.h><BR>#include <SYS ipc.h><BR>#include <SYS shm.h><BR><BR>#define PERM S_IRUSR|S_IWUSR <BR><BR>int main(int argc,char **argv) <BR>{ <BR><BR>int shmid; <BR>char *p_addr,*c_addr; <BR>if(argc!=2) <BR>{ <BR>fprintf(stderr,\"Usage:%s\\n\\a\",argv[0]); <BR>exit(1); <BR>} <BR><BR>if((shmid=shmget(IPC_PRIVATE,1024,PERM))==-1) <BR>{ <BR>fprintf(stderr,\"Create Share Memory Error:%s\\n\\a\",strerror(errno)); <BR>exit(1); <BR>} <BR>if(fork()) <BR>{ <BR>p_addr=shmat(shmid,0,0); <BR>memset(p_addr,\\\0\,1024); <BR>strncpy(p_addr,argv[1],1024); <BR>exit(0); <BR>} <BR>else <BR>{ <BR>c_addr=shmat(shmid,0,0); <BR>printf(\"Client get %s\",c_addr); <BR>exit(0); <BR>} <BR>} <BR><BR>这个程序是父进程将参数写入到共享内存,然后子进程把内容读出来.最后我们要使用ipcrm释放资源的.先用ipcs找出ID然后用ipcrm shm ID删除. <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/498.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:56 <a href="http://www.cnitblog.com/drizztzou/articles/498.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>轻轻松松产生 Makefile</title><link>http://www.cnitblog.com/drizztzou/articles/497.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:52:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/497.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/497.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/497.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/497.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/497.html</trackback:ping><description><![CDATA[在 Unix 上写程式的人大概都碰过 Makefile，尤其是用 C 来开发程式的人。用 make 来开发和编译程式的确很方便，可是要写出一个 Makefile 就不简单了。偏偏介绍 Makefile 的文件不多，GNU Make 那份印出来要几 百页的文件，光看完 Overview 就快阵亡了，难怪许多人闻 Unix 色变。 <BR><BR>本文将介绍如何利用 GNU Autoconf 及 Automake 这两套软体来协助我们 『自动』产生 Makefile 档，并且让开发出来的软体可以像 Apache, MySQL 和常见的 GNU 软体一样，只要会 ``./configure\\, ``make\\, ``make install\\ 就可以把程式安装到系统中。如果您有心开发 Open Source 的软体，或只是想在 Unix 系统下写写程式。希望这份介绍文件能 帮助您轻松地进入 Unix Programming 的殿堂。 <BR><BR><BR>1. 简介 <BR><BR><BR>Makefile 基本上就是『目标』(target), 『关连』(dependencies) 和 『动作』三者所组成的一连串规则。而 make 就会根据 Makefile 的规则 来决定如何编译 (compile) 和连结 (link) 程式。实际上，make 可做的 不只是编译和连结程式，例如 FreeBSD 的 port collection 中， <BR><BR>Makefile 还可以做到自动下载原始程式套件，解压缩 (extract) ，修补 (patch)，设定，然後编译，安装至系统中。 <BR><BR>Makefile 基本构造虽然简单，但是妥善运用这些规则就也可以变出许多不 同的花招。却也因此，许多刚开始学习写 Makefile 时会感到没有规范可 循，每个人写出来的 Makefile 长得都不太一样，不知道从何下手，而且 常常会受限於自己的开发环境，只要环境变数不同或路径改一下，可能 Makefile 就得跟着修改。虽然有 GNU Makefile Conventions (GNU Makefile 惯例) 订出一些使用 GNU 程式设计时撰写 Makefile 的一些标 准和规范，但是内容很长而且很复杂, 并且经常做些调整，为了减轻程式 设计师维护 Makefile 的负担，因此有了 Automake。 <BR><BR><BR>程式设计师只需写一些预先定义好的巨集 (macro)，交给 Automake 处理 後会产生一个可供 Autoconf 使用的 Makefile.in 档。再配合利用 Autoconf 产生的自动设定档 configure 即可产生一份符合 GNU Makefile <BR><BR>惯例的 Makeifle 了。 <BR><BR><BR>2. 上路之前 <BR><BR><BR>在开始试着用 Automake 之前，请先确认你的系统已经安装以下的软体： <BR>1. GNU Automake <BR>2. GNU Autoconf <BR>3. GNU m4 <BR>4. perl <BR>5. GNU Libtool (如果你需要产生 shared library) <BR>我会建议你最好也使用 GNU C/C++ 编译器 、GNU Make 以及其它 GNU 的 工具程式来做为开发的环境，这些工具都是属於 Open Source Software不仅免费而且功能强大。如果你是使用 Red Hat Linux 可以找到所有上述软体的 rpm 档，FreeBSD 也有现成的 package 可以直接安装，或着你也 可以自行下载这些软体的原始档回来 DIY。以下的范例是在 Red Hat Linux 5.2 + CLE2 的环境下所完成的。 <BR><BR><BR>3. 一个简单的例子 <BR><BR><BR>Automake 所产生的 Makefile 除了可以做到程式的编译和连结，也已经把 如何产生程式文件 (如 manual page, info 档及 dvi 档) 的动作，还有 把原始程式包装起来以供散　的动作都考虑进去了，所以原始程式所存放 的目录架构最好符合 GNU 的标准惯例，接下来我拿 hello.c 来做为例 子。 <BR><BR><BR>在工作目录下建立一个新的子目录 ``devel\\，再在 devel 下建立一个 ``hello\\ 的子目录，这个目录将作为我们存放 hello 这个程式及其相关档案的地方： <BR><BR><BR>% mkdir devel <BR>% cd devel <BR>% mkdir hello <BR>% cd hello <BR><BR><BR>用编辑器写个 hello.c 档， <BR><BR>#include stdio.h <BR><BR>int main(int argc, char** argv) <BR><BR>{ <BR>printf(``Hello, GNU! \\); <BR><BR>return 0; <BR><BR>} <BR><BR>接下来就要用 Autoconf 及 Automake 来帮我们产生 Makefile 档了， 1. 用 autoscan 产生一个 configure.in 的雏型，执行 autoscan 後会产 生一个configure.scan 的档案，我们可以用它做为 configure.in 档的蓝本。 <BR><BR><BR>% autoscan <BR><BR>% ls <BR><BR>configure.scan hello.c <BR><BR><BR>2. 编辑 configure.scan 档，如下所示，并且把它的档名改成 <BR><BR>configure.indnl Process this file with autoconf to produce a configure script. <BR><BR><BR>AC_INIT(hello.c) <BR><BR>AM_INIT_AUTOMAKE(hello, 1.0) <BR>dnl Checks for programs. <BR><BR>AC_PROG_CC <BR><BR>dnl Checks for libraries. <BR><BR>dnl Checks for header files. <BR><BR>dnl Checks for typedefs, structures, and compiler characteristics. <BR>dnl Checks for library functions. <BR><BR>AC_OUTPUT(Makefile) <BR><BR>3. 执行 aclocal 和 autoconf ，分别会产生 aclocal.m4 及 configure 两 个档案 <BR><BR>% aclocal <BR><BR>% autoconf <BR><BR>% ls <BR><BR>aclocal.m4 configure configure.in hello.c <BR><BR>4. 编辑 Makefile.am 档，内容如下 <BR><BR>AUTOMAKE_OPTIONS= foreign <BR><BR><BR>bin_PROGRAMS= hello <BR><BR><BR>hello_SOURCES= hello.c <BR><BR><BR>5. 执行 automake --add-missing ，Automake 会根据 Makefile.am 档产生 一些档案，包含最重要的 Makefile.in <BR><BR><BR>% automake --add-missing <BR><BR>automake: configure.in: installing `./install-sh\ <BR>automake: configure.in: installing `./mkinstalldirs\ <BR>automake: configure.in: installing `./missing\ <BR><BR>6. 最後执行 ./configure ， <BR><BR><BR>% ./configure <BR><BR><BR>creating cache ./config.cache <BR><BR>checking for a BSD compatible install... /usr/bin/install -c <BR><BR>checking whether build environment is sane... yes <BR><BR>checking whether make sets ${MAKE}... yes <BR><BR>checking for working aclocal... found <BR><BR>checking for working autoconf... found <BR><BR>checking for working automake... found <BR><BR>checking for working autoheader... found <BR><BR>checking for working makeinfo... found <BR><BR>checking for gcc... gcc <BR><BR>checking whether the C compiler (gcc ) works... yes <BR><BR><BR>checking whether the C compiler (gcc ) is a cross-co <BR>mpiler... no <BR><BR>checking whether we are using GNU C... yes <BR><BR>checking whether gcc accepts -g... yes <BR><BR>updating cache ./config.cache <BR><BR>creating ./config.status <BR><BR>creating Makefile <BR><BR><BR>现在你的目录下已经产生了一个 Makefile 档，下个 ``make\\ 指令就可 <BR><BR>以开始编译 hello.c 成执行档，执行 ./hello 和 GNU 打声招呼吧！ <BR><BR><BR>% make <BR><BR><BR>gcc -DPACKAGE=\"hello\" -DVERSION=\"1.0\" -I. -I. -g -O2 -c he <BR>llo.c <BR><BR><BR>gcc -g -O2 -o hello hello.o <BR><BR><BR>% ./hello <BR><BR><BR>Hello! GNU! <BR><BR><BR>你还可以试试 ``make clean\\，\\make install\\，\\make dist\\ 看看 <BR><BR>会有什麽结果。你也可以把产生出来的 Makefile 秀给你的老板，让他从 <BR><BR>此对你刮目相看 :-) <BR><BR><BR>4. 一探究竟 <BR><BR><BR>上述产生 Makefile 的过程和以往自行编写的方式非常不一样，舍弃传统 <BR><BR>自行定义 make 的规则，使用 Automake 只需用到一些已经定义好的巨集 <BR><BR>即可。我们把巨集及目标 (target) 写在 Makefile.am 档内，Automake <BR><BR>读入 Makefile.am 档後会把这一串已经定义好的巨集展开并且产生对应的 <BR><BR>Makefile.in 档， 然後再由 configure 这个 shell script 根据 <BR><BR>Makefile.in 产生适合的 Makefile。 <BR><BR><BR><BR>[Figure 1:利用 autoconf 及 automake 产生 Makefile 的流程] <BR><BR><BR>上图中表示在上一节范例中所要用的档案以及产生出来的档案，有星号 <BR><BR>(*) 者代表可执行档。在此范例中可藉由 Autoconf 及 Automake 工具所 <BR><BR>产生的档案有 configure.scan、aclocal.m4、configure、Makefile.in， <BR><BR>需要我们加入设定者为 configure.in 及 Makefile.am。 <BR><BR><BR>4.1 编辑 configure.in 档 <BR><BR><BR>Autoconf 是用来产生 \configure\ 档的工具。\configure\ 是一个 <BR><BR>shell script，它可以自动设定原始程式以符合各种不同平台上 Unix 系 <BR><BR>统的特性，并且根据系统叁数及环境产生合适的 Makefile 档或是C 的标 <BR><BR>头档 (header file)，让原始程式可以很方便地在这些不同的平台上被编 <BR><BR>译出来。Autoconf 会读取 configure.in 档然後产生 \configure\ 这个 <BR><BR>shell script。 <BR><BR><BR>configure.in 档的内容是一连串 GNU m4 的巨集，这些巨集经过 <BR><BR>autoconf 处理後会变成检查系统特徵的 shell script。configure.in 内 <BR><BR>巨集的顺序并没有特别的规定，但是每一个 configure.in 档必须在所有 <BR><BR>巨集前加入 AC_INIT 巨集，然後在所有巨集的最後面加上 AC_OUTPUT 巨 <BR><BR>集。我们可先用 autoscan 扫描原始档以产生一个 configure.scan 档， <BR><BR>再对 configure.scan 做些修改成 configure.in 档。在范例中所用到的 <BR><BR>巨集如下： <BR><BR><BR>dnl <BR><BR>这个巨集後面的字不会被处理，可视为注解。 <BR><BR>AC_INIT(FILE) <BR><BR>这个巨集用来检查原始码所在的路径，autoscan 会自动产生，我们 <BR><BR>不必修改它。 <BR><BR>AM_INIT_AUTOMAKE(PACKAGE,VERSION) <BR><BR>这是使用 Automake 所必备的巨集，PACKAGE 是我们所要产生软体套 <BR><BR>件的名称，VERSION 是版本编号。 <BR><BR>AC_PROG_CC <BR><BR>检查系统可用的 C 编译器，如果原始程式是用 C 写的就需要这个巨 <BR><BR>集。 <BR><BR>AC_OUTPUT(FILE) <BR><BR>设定 configure 所要产生的档案，如果是 Makefile 的话， <BR><BR>configure 便会把它检查出来的结果带入 Makefile.in 档然後产生 <BR><BR>合适的 Makefile。 <BR><BR><BR>实际上，我们使用 Automake 时，还须要一些其它的巨集，这些额外的巨 <BR><BR>集我们用 aclocal 来帮我们产生。执行 aclocal 会产生 aclocal.m4 <BR><BR>档，如果没有特别的用途，我们可以不必修改它，用 aclocal 所产生的巨 <BR><BR>集会告诉 Automake 怎麽做。 <BR><BR><BR>有了 configure.in 及 aclocal.m4 两个档案後，便可以执行 autoconf <BR><BR>来产生 configure 档了。 <BR><BR><BR>4.2 编辑 Makefile.am 档 <BR><BR><BR>接下来我们要编辑 Makefile.am 档，Automake 会根据 configure.in 中 <BR><BR>的巨集把Makefile.am 转成 Makefile.in 档。Makefile.am 档定义我们所 <BR><BR>要产的目标： <BR><BR><BR>AUTOMAKE_OPTIONS <BR><BR>设定 automake 的选项。Automake 主要是帮助开发 GNU 软体的人员 <BR><BR>维护软体套件，所以在执行 automake 时，会检查目录下是否存在标 <BR><BR>准 GNU 软体套件中应具备的文件档案，例如 \NEWS\、\AUTHOR\、 <BR><BR>\ChangeLog\ 等文件档。设成 foreign 时，automake 会改用一般软 <BR><BR>体套件的标准来检查。 <BR><BR>bin_PROGRAMS <BR><BR>定义我们所要产生的执行档档名。如果要产生多个执行档，每个档名 <BR><BR>用空白字元隔开。 <BR><BR>hello_SOURCES <BR><BR>定义 \hello\ 这个执行档所需要的原始档。如果 \hello\ 这个程式 <BR><BR>是由多个原始档所产生，必须把它所用到的原始档都列出来，以空白 <BR><BR>字元隔开。假设 \hello\ 这个程式需要 \hello.c\、\main.c\、 <BR><BR>\hello.h\ 三个档案的话，则定义 <BR><BR><BR>hello_SOURCES= hello.c main.c hello.h <BR><BR>如果我们定义多个执行档，则对每个执行档都要定义相对的 <BR><BR>filename_SOURCES。 <BR><BR><BR>编辑好 Makefile.am 档，就可以用 automake --add-missing 来产生 <BR><BR>Makefile.in。加上 --add-missing 选项是告诉 automake 顺便帮我们加 <BR><BR>入包装一个软体套件所必备的档案。Automake 产生出来的 Makefile.in <BR><BR>档是完全符合 GNU Makefile 的惯例，我们只要执行 configure 这个 <BR><BR>shell script 便可以产生合适的 Makefile 档了。 <BR><BR><BR>4.3 使用 Makefile <BR><BR><BR>利用 configure 所产生的 Makefile 档有几个预设的目标可供使用，我们 <BR><BR>只拿其中几个简述如下： <BR><BR><BR>make all <BR><BR>产生我们设定的目标，即此范例中的执行档。只打 make 也可以，此 <BR><BR>时会开始编译原始码，然後连结，并且产生执行档。 <BR><BR>make clean <BR><BR>清除之前所编译的执行档及目的档 (object file, *.o)。 <BR><BR>make distclean <BR><BR>除了清除执行档和目的档外，也把 configure 所产生的 Makefile <BR><BR>也清除掉。 <BR><BR>make install <BR><BR>将程式安装至系统中。如果原始码编译无误，且执行结果正确，便可 <BR><BR>以把程式安装至系统预设的执行档存放路径。如果我们用 <BR><BR>bin_PROGRAMS 巨集的话，程式会被安装至 /usr/local/bin 这个目 <BR><BR>录。 <BR><BR>make dist <BR><BR>将程式和相关的档案包装成一个压缩档以供散播 (distribution) 。 <BR><BR>执行完在目录下会产生一个以 PACKAGE-VERSION.tar.gz 为名称的档 <BR><BR>案。PACKAGE 和 VERSION 这两个变数是根据 configure.in 档中 <BR><BR>AM_INIT_AUTOMAKE(PACKAGE, VERSION) 的定义。在此范例中会产生 <BR><BR>\hello-1.0.tar.gz\ 的档案。 <BR><BR>make distcheck <BR><BR>和 make dist 类似，但是加入检查包装後的压缩档是否正常。这个 <BR><BR>目标除了把程式和相关档案包装成 tar.gz 档外，还会自动把这个压 <BR><BR>缩档解开，执行 configure，并且进行 make all 的动作，确认编译 <BR><BR>无误後，会显示这个 tar.gz 档已经准备好可供散播了。这个检查非 <BR><BR>常有用，检查过关的套件，基本上可以给任何一个具备 GNU 发展环 <BR><BR>境的人去重新编译。就 hello-1.tar.gz 这个范例而言，除了在 Red <BR><BR>Hat Linux 上，在 FreeBSD 2.2.x 版也可以正确地重新编译。 <BR><BR><BR>要注意的是，利用 Autoconf 及 Automake 所产生出来的软体套件是可以 <BR><BR>在没有安装 Autoconf 及 Automake 的环境上使用的，因为 configure 是 <BR><BR>一个 shell script，它己被设计可以在一般 Unix 的 sh 这个 shell 下 <BR><BR>执行。但是如果要修改 configure.in 及 Makefile.am 档再产生新的 <BR><BR>configure 及 Makefile.in 档时就一定要有 Autoconf 及 Automake 了。 <BR><BR><BR>5. 相关讯息 <BR><BR><BR>Autoconf 和 Automake 功能十分强大，你可以从它们所附的 info 档找到 <BR><BR>详细的用法。你也可以从许多现存的 GNU 软体或 Open Source 软体中找 <BR><BR>到相关的 configure.in 或 Makefile.am 档，它们是学习 Autoconf 及 <BR><BR>Automake 更多技巧的最佳范例。 <BR><BR><BR>这篇简介只用到了 Autoconf 及 Automake 的皮毛罢了，如果你有心加入 <BR><BR>Open Source 软体开发的行列，希望这篇文件能帮助你对产生 Makefile <BR><BR>有个简单的依据。其它有关开发 GNU 程式或 C 程式设计及 Makefile 的 <BR><BR>详细运用及技巧，我建议你从 GNU Coding Standards3 (GNU 编码标准规 <BR><BR>定) 读起，里面包含了 GNU Makefile 惯例，还有发展 GNU 软体套件的标 <BR><BR>准程序和惯例。这些 GNU 软体的线上说明文件可以在 <BR><BR><A style="COLOR: #003793" href="http://www.gnu.org/" target=_blank>http://www.gnu.org/</A> 这个网站上找到。 <BR><BR><BR>6. 结语 <BR><BR><BR>经由 Autoconf 及 Automake 的辅助，产生一个 Makefile 似乎不再像以 <BR><BR>前那麽困难了，而使用 Autoconf 也使得我们在不同平台上或各家 Unix <BR><BR>之间散播及编译程式变得简单，这对於在 Unix 系统上开发程式的人员来 <BR><BR>说减轻了许多负担。妥善运用这些 GNU 的工具软体，可以帮助我们更容易 <BR><BR>去发展程式，而且更容易维护原始程式码。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/497.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:52 <a href="http://www.cnitblog.com/drizztzou/articles/497.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux 的 x86 汇编程序设计</title><link>http://www.cnitblog.com/drizztzou/articles/495.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:47:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/495.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/495.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/495.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/495.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/495.html</trackback:ping><description><![CDATA[本质上来说, 这篇文章是把我最感兴趣的两样编程东西: Linux 操作系统和汇编语言程序设计结合在一起. 这两个都不(或者说应该不)需要介绍; 像 Win32 的汇编,Linux 的汇编运行在 32 位的保护模式下...但它又有一个截然不同的优势就是它允许你调用 C 的标准库函数和 Linux 的共享库函数. 我开始给 Linux 下的汇编语言编程来个简要介绍; 为了更好读一点, 你可能要跳过这个基本的小节. <BR><BR>编译和链接 <BR>--------------------- <BR>Linux 下两个最主要的汇编器是 Nasm(free, Netwide Assembler)和 GAS(free, Gnu Assembler), <BR>后一个和 GCC 结合在一起. 在这篇文章里我将集中在 Nasm 上, 把 GAS 放在后面,因为它使用 AT&amp;T 的语法, 需要一个长的介绍. <BR>Nasm 调用时应该带上 ELF 格式选项("nasm -f elf hello.asm"); 产生的目标文件用GCC 来链接("gcc hello.o"), 产生最终的 ELF 二进制代码. 下面的这个脚本可用来编译 ASM 的模块; 我尽量把它写得简单, 所以所有它做的就是接受传给它的第一个文件名, 用 Nasm 编译, 用 GCC 来链接. <BR>#!/bin/sh <BR># assemble.sh ========================================================= <BR>outfile=${1%%.*} <BR>tempfile=asmtemp.o <BR>nasm -o $tempfile -f elf $1 <BR>gcc $tempfile -o $outfile <BR>rm $tempfile -f <BR>#EOF ================================================================= <BR><BR>基本知识: <BR>---------- <BR>当然最好的就是在了解系统细节之前从一个例子开始. 这里是一个最基本的"hello-word" 形式的程序: <BR>; asmhello.asm ======================================================== <BR>global main <BR>extern printf <BR>section .data <BR>msg db "Helloooooo, nurse!",0Dh,0Ah,0 <BR>section .text <BR>main: <BR>push dword msg <BR>call printf <BR>pop eax <BR>ret <BR>; EOF ================================================================= <BR>纲要: "global main" 必须声明为全局的(global) -- 并且既然我们用 GCC 来链接,进入点必须以 "main" 来命名 -- 从而装入系统. "extern printf" 只是一个声明,为以后在程序中调用; 注意这是必须的; 参数的大小不需要声明. 我已经把这个例子用标准的 .data, .text 分节, 但这不是严格必须的 -- 可能只需要一个 .text段, 就像在 DOS 下一样. <BR>在代码的主体部分, 你必须把参数压栈来传递给调用. 在 Nasm 里, 你必须声明所有不明确数据的大小; 因此就有 "dword" 这个限定词. 注意和其他汇编器一样,Nasm 假设所有的内存/标号的引用都指的是内存地址或者标号, 而不是它的内容. <BR>因而, 指明字符串 msg 的地址, 你应该使用 push dword msg, 指明字符串 msg 的内容, 应该用 push dword [msg] (这只能包含 msg 的前四个字节). 因为 printf <BR>需要一个指向字符串的指针, 我们应该指明 msg 的地址. <BR>调用 printf 非常的直接. 注意每一次调用后你必须把栈清除(见下); 所以 PUSH 了一个 <BR>dword 后, 我从栈里把一个 dword POP 进一个无用的寄存器. Linux 程序只简单的用一个 RET 来返回系统, 由于每个进程都是 shell(或者是 PID)的产物, 所以程序结束后把 控制权还给它. <BR>注意到在 Linux 下, 你是在 "API" 或中断服务的场所里使用系统带来的标准共享库. <BR><BR>所有的外部引用由 GCC 管理, 它给 asm 程序员节省了大部分的工作. 一旦你习惯了基本的技巧, Linux 下的汇编编程实际上要比 DOS 简单的多. <BR><BR>C 调用的语法 <BR>-------------------- <BR>Linux 使用 C 的调用模式 -- 意味着参数以相反的顺序进栈(最后一个最先), 调用者必须清 <BR>除栈. 你可以从栈里把值 pop 出来: <BR>push dword szText <BR>call puts <BR>pop ecx <BR>或者直接修改 ESP: <BR>push dword szText <BR>call puts <BR>add esp, 4 <BR>调用的返回值在 eax 或 edx:eax 如果值大于 32 位的话. EBP, ESI, EDI, EBX 由调用者 <BR>保存和恢复. 你必须保存你要使用的寄存器, 像下面这样: <BR>; loop.asm ================================================================= <BR><BR>global main <BR>extern printf <BR>section .text <BR>msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0 <BR>main: <BR>mov ecx, 0Ah <BR>push dword msg <BR>looper: <BR>call printf <BR>loop looper <BR>pop eax <BR>ret <BR>; EOF ================================================================ <BR><BR>粗一看, 非常简单: 因为你在 10 个 printf() 调用用的是同一个字符串, 你不需要清除栈. 但当你编译以后, 循环不会停止. 为什么? 因为 printf() 里什么地方用了 ECX 但没有保存. 使你的循环正确的工作, 你必须在调用之前保存 ECX 的值, 调用之后恢复它, 像这样: <BR>; loop.asm ================================================================ <BR>global main <BR>extern printf <BR>section .text <BR>msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0 <BR>main: <BR>mov ecx, 0Ah <BR>looper: <BR>push ecx ;save Count <BR>push dword msg <BR>call printf <BR>pop eax ;cleanup stack <BR>pop ecx ;restore Count <BR>loop looper <BR>ret <BR>; EOF ================================================================ <BR><BR>I/O 端口编程 <BR>-------------------- <BR>但直接访问硬件会怎么样呢? 在 Linux 下你需要一个核心模式的驱动程序来做这些工作... 这意味着你的程序必须分成两个部分, 一个核心模式提供硬件直接操作的功能, 其他的用户模式提供接口. 一个好消息就是你仍然可以在用户模式的程序中使用IN/OUT 来访问端口. <BR>要访问端口你的程序必须取得系统的同意; 要做这个, 你必须调用 ioperm(). 这个函数只能被有 root 权限的用户使用, 所以你必须用 setuid() 使程序到 root 或者直接运行在 root 下. ioperm() 的语法是这样: <BR>ioperm( long StartingPort#, long #Ports, BOOL ToggleOn-Off) <BR>StartingPort# 指明要访问的第一个端口值(0 是端口 0h, 40h 是端口 40h, 等等),#Ports <BR>指明要访问多少个端口(也就是说, StartingPort# = 30h, #Port = 10, 可以访问端口 <BR>30h - 39h), ToggleOn-Off 如果是 TRUE(1) 就能够访问, 是 FALSE(0) 就不能访问. <BR>一旦调用了 ioperm(), 要求的端口就和平常一样访问. 程序可以调用 ioperm() 任意多次, <BR>而不需要在后来调用 ioperm()(但下面的例子这样做了), 因为系统会处理这些. <BR>; io.asm ============================================================== <BR>= <BR>BITS 32 <BR>GLOBAL szHello <BR>GLOBAL main <BR>EXTERN printf <BR>EXTERN ioperm <BR>SECTION .data <BR>szText1 db Enabling I/O Port Access,0Ah,0Dh,0 <BR>szText2 db Disabling I/O Port Acess,0Ah,0Dh,0 <BR>szDone db Done!,0Ah,0Dh,0 <BR>szError db Error in ioperm() call!,0Ah,0Dh,0 <BR>szEqual db Output/Input bytes are equal.,0Ah,0Dh,0 <BR>szChange db Output/Input bytes changed.,0Ah,0Dh,0 <BR>SECTION .text <BR>main: <BR>push dword szText1 <BR>call printf <BR>pop ecx <BR>enable_IO: <BR>push word 1 ; enable mode <BR>push dword 04h ; four ports <BR>push dword 40h ; start with port 40 <BR>call ioperm ; Must be SUID "root" for this call! <BR>add ESP, 10 ; cleanup stack (method 1) <BR>cmp eax, 0 ; check ioperm() results <BR>jne Error <BR>;---------------------------------------Port Programming Part-------------- <BR>SetControl: <BR>mov al, 96 ; R/W low byte of Counter2, mode 3 <BR>out 43h, al ; port 43h = control register <BR>WritePort: <BR>mov bl, 0EEh ; value to send to speaker timer <BR>mov al, bl <BR>out 42h, al ; port 42h = speaker timer <BR>ReadPort: <BR>in al, 42h <BR>cmp al, bl ; byte should have changed--this IS a timer :) <BR>jne ByteChanged <BR>BytesEqual: <BR>push dword szEqual <BR>call printf <BR>pop ecx <BR>jmp disable_IO <BR>ByteChanged: <BR>push dword szChange <BR>call printf <BR>pop ecx <BR>;---------------------------------------End Port Programming Part---------- <BR>disable_IO: <BR>push dword szText2 <BR>call printf <BR>pop ecx <BR>push word 0 ; disable mode <BR>push dword 04h ; four ports <BR>push dword 40h ; start with port 40h <BR>call ioperm <BR>pop ecx ;cleanup stack (method 2) <BR>pop ecx <BR>pop cx <BR>cmp eax, 0 ; check ioperm() results <BR>jne Error <BR>jmp Exit <BR>Error: <BR>push dword szError <BR>call printf <BR>pop ecx <BR>Exit: <BR>ret <BR>; EOF ====================================================================== <BR><BR>在 Linux 下使用中断 <BR>------------------------- <BR>Linux 是一个运行在保护模式下的共享库的环境, 意味着没有中断服务, Right? <BR>错了. 我注意到在 GAS 的例子源码中用了 INT 80, 注释是 "sys_write(ebx, ecx, ed <BR>x)". <BR>这个函数是 Linux 系统调用接口的一部分, 意思是 INT 80 必须是到达系统调用服务 <BR>的门户. 在 Linux 源码中到处看时(忽略从不要使用 INT 80 接口的警告, 因为函数号 <BR><BR>可能随时改变), 我发现 "系统调用号(system call numbers)" -- 就是说, 传给 INT <BR>80 <BR>的 # 对应着一个系统调用子程序 -- 在 UNISTD.H 中. 一共有 189 个, 所以我不会在 <BR><BR>这里列出来...但如果你在 Linux 做汇编, 给自己做个好事, 打印出来吧. <BR>当调用 INT 80 时, eax 设为用调用的功能号. 传给系统调用则程序的参数必须按顺序 <BR><BR>放在下列寄存器中: <BR>ebx, ecx, edx, esi, edi <BR>这样, 第一个参数就在 ebx 里, 第二个在 ecx 里... 注意在一个系统调用程序里, 不 <BR>是 <BR>用栈来传递参数. 调用的返回值在 eax 里. <BR>还有, INT 80 接口和一般的调用一样. 下面的这个程序就演示了 INT 80h 的使用. 这 <BR>个 <BR>程序检查并显示了它自己的 PID. 注意 使用 printf() 格式化字符串 -- 这个调用的 <BR>C 结构 <BR>是: <BR>printf( "%d\n", curr_PID); <BR>也要注意结束符在汇编里不一定可靠, 我常用十六进制(0Ah, 0Dh)代表 CR\LF. <BR>;pid.asm==================================================================== <BR><BR>BITS 32 <BR>GLOBAL main <BR>EXTERN printf <BR>SECTION .data <BR>szText1 db Getting Current Process ID...,0Ah,0Dh,0 <BR>szDone db Done!,0Ah,0Dh,0 <BR>szError db Error in int 80!,0Ah,0Dh,0 <BR>szOutput db \%d,0Ah,0Dh,0 ;printf() 的格式字符串 <BR>SECTION .text <BR>main: <BR>push dword szText1 ;开始信息 <BR>call printf <BR>pop ecx <BR>GetPID: <BR>mov eax, dword 20 ; getpid() 系统调用 <BR>int 80h ; 系统调用中断 <BR>cmp eax, 0 ; 没有 PID 0 ! :) <BR>jb Error <BR>push eax ; 把返回值传递给 printf <BR>push dword szOutput ; 把格式字符串传递给 printf <BR>call printf <BR>pop ecx ; 清除栈 <BR>pop ecx <BR>push dword szDone ; 结束信息 <BR>call printf <BR>pop ecx <BR>jmp Exit <BR>Error: <BR>push dword szError <BR>call printf <BR>pop ecx <BR>Exit: <BR>ret <BR>; EOF ===================================================================== <BR>最后的话 <BR>----------- <BR>大多数的麻烦来自对 Nasm 的习惯上. 而 nasm 带有手册, 但缺省是不安装的, <BR>所以你必须把它从 <BR>/user/local/bin/nasm-0.97/nasm.man <BR>移(cp 或 mv)到 <BR>/usr/local/man/man1/nasm.man. <BR>格式有点乱, 可以很简单的用 nroff 指示符来解决. 但它不会给你 Nasm 的整个文 <BR>档; 要解决这个问题, 把 nasmdoc.txt 从 <BR>/usr/local/bin/nasm-0.97/doc/nasmdoc.txt <BR>拷贝到 <BR>/usr/local/man/man1/nasmdoc.man <BR>现在你可以用 man nasm, man nasmdoc 来看 nasm 的手册和文档了 <BR>想得到更多的信息, 查查这里: <BR>Linux Assembly Language HOWTO (Linux 汇编语言 HOWTO) <BR>Linux I/O Port Programming Mini-HOWTO (Linux I/O 端口编程 Mini-HOWTO) <BR>Jans Linux &amp; Assembler HomePage (<A class=red href="http://www.bewoner.dma.be/JanW/eng.html" target=_blank><A style="COLOR: #003793" href="http://www.bewoner.dma.be/JanW/eng.html" target=_blank>http://www.bewoner.dma.be/JanW/eng.html</A></A>) <BR>我也要感谢 Jeff Weeks(<A class=red href="http://gameprog.com/codex" target=_blank><A style="COLOR: #003793" href="http://gameprog.com/codex" target=_blank>http://gameprog.com/codex</A></A>), 在我找到 Jan 的网页之前 <BR>给了我一些 GAS 的 hello-world 代码. <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/495.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:47 <a href="http://www.cnitblog.com/drizztzou/articles/495.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux程式设计入门 fork/pthread/signals</title><link>http://www.cnitblog.com/drizztzou/articles/494.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:46:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/494.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/494.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/494.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/494.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/494.html</trackback:ping><description><![CDATA[fork()及signal经常运用在daemon守护神这一类常驻程式，另外像a4c.tty/yact/chdrv这些中文终端机程式也有用到，一般如Mozilla/Apache/Squid等大程式几乎都一定会用到。 <BR><BR><BR>虽然在UNIX下的程式写作，对thread的功能需求并非很大，但thread在现代的作业系统中，几乎都已经存在了。pthread是Linux上的thread函数库，如果您要在Linux下撰写多线程式，例如MP3播放程式，熟悉pthread的用法是必要的。 <BR><BR><BR>pthread及signal都可以用一大章来讨论。在这里，我只谈及最简单及常用的技巧，当您熟悉这些基本技巧的运用後，再找一些专门深入探讨pthread及signal程式写作的书籍来研究。这些进阶的写法，用到的机会较少，将层次分明，学习速度应该会比较快。 <BR><BR>程序分歧fork() <BR><BR>fork()会产生一个与父程序相同的子程序，唯一不同之处在於其processid(pid)。 <BR><BR><BR>如果我们要撰写守护神程式，或是例如网路伺服器，需要多个行程来同时提供多个连线，可以利用fork()来产生多个相同的行程。 <BR><BR><BR>函数宣告 <BR><BR>pid_tfork(void); <BR><BR>pid_tvfork(void); <BR><BR><BR>返回值: <BR><BR><BR>-1:失败。 <BR><BR>0:子程序。 <BR><BR>&gt;0:将子程序的processid传回给父程序。 <BR><BR><BR>在Linux下fork()及vfork()是相同的东西。 <BR><BR><BR>范例一:fork.c <BR><BR><BR>在这个范例中，我们示范fork()的标准用法。 <BR><BR><BR>#include <BR><BR>#include <BR><BR>#include <BR><BR><BR>voidmain(void) <BR><BR>{ <BR><BR>pid_tpid; <BR><BR><BR>printf("hello\n"); <BR><BR>pid=fork(); <BR><BR><BR>switch(pid){ <BR><BR>case-1:printf("failure!\n");break; <BR><BR>case0:printf("Iamchild!\n");break; <BR><BR>default:printf("mychildis%d\n",pid);break; <BR><BR>} <BR><BR>for(;;){/*dosomethinghere*/} <BR><BR>} <BR><BR><BR>编译: <BR><BR>gcc-oex1fork.c <BR><BR>执行结果: <BR><BR>./ex1&amp; <BR><BR>hello <BR>mychildis8650 <BR><BR>Iamchild! <BR><BR><BR>我们可以见到，使用fork()，可将一个程式分岐成两个。在分歧之前的程式码只执行一次。 <BR><BR><BR>检验行程: <BR><BR>ps|grepex1 <BR><BR><BR>8649p0R0:40./ex1 <BR><BR>8650p0R0:40./ex1 <BR><BR><BR>8649是父程序的pid，8650则为子程序的pid。 <BR><BR>您会需要用到"killallex1"来杀掉两个行程。 <BR><BR><BR>范例二:daemon.c <BR><BR><BR>在UNIX中，我们一般都利用fork()，来实作所谓的"守护神程式"，也就是DOS中所谓的"常驻程式"。一般的技巧是将父程序结束，而子程序便成为"守护神"。 <BR><BR>这个范例中，示范一般标准的daemon写法。 <BR><BR>#include <BR><BR>#include <BR><BR>#include <BR><BR><BR>voidmain(void) <BR><BR>{ <BR><BR>pid_tpid; <BR><BR><BR>pid=fork(); <BR><BR><BR>if(pid&gt;0){ <BR><BR>printf("daemononduty!\n"); <BR><BR>exit(0); <BR><BR>}else <BR><BR>if(pid&lt;0){ <BR><BR>printf("Can fork!\n"); <BR><BR>exit(-1); <BR><BR>} <BR><BR><BR>for(;;){ <BR><BR>printf("Iamthedaemon!\n"); <BR><BR>sleep(3); <BR><BR>/*dosomethingyourownhere*/ <BR><BR>} <BR><BR><BR>} <BR><BR><BR>编译: <BR><BR><BR>gcc-oex2daemon.c <BR><BR><BR>执行结果: <BR><BR><BR>./ex2 <BR><BR><BR>daemononduty! <BR><BR>Iamthedaemon! <BR><BR>接下来每三秒钟，都会出现一个"Iamthedaemon!"的讯息，这表示您的程式已经"长驻"在系统中了。 <BR><BR>检验行程: <BR><BR>ps|grepex2 <BR><BR>8753p0S0:00./ex2 <BR><BR><BR>注意到在范例一中，我们下的指令为"./ex1&amp;"，而在范例二中为"./ex2"，没有"&amp;"符号。 <BR><BR><BR>范例三:lock.c <BR><BR><BR>许多的时候，我们希望"守护神"在系统中只有一个，这时候会需要用到pidlock的技巧。如果您注意到/var/run目录中的内容，您会发现到有许多的*.pid档，观看其内容都是一些数字，这些数字其实就是该行程的pid。 <BR><BR>#include <BR><BR>#include <BR><BR>#include <BR><BR>voidmain(void) <BR><BR>{ <BR><BR>FILE*fp; <BR><BR>pid_tpid; <BR><BR>exit(-1); <BR><BR>} <BR><BR>act.sa_handler=quit; <BR>act.sa_flags=0; <BR>sigemptyset(&amp;act.sa_mask); <BR>sigaction(SIGTERM,&amp;act,NULL); <BR>sigaction(SIGHUP,&amp;act,NULL); <BR>sigaction(SIGINT,&amp;act,NULL); <BR><BR>sigaction(SIGQUIT,&amp;act,NULL); <BR><BR>sigaction(SIGUSR1,&amp;act,NULL); <BR><BR>sigaction(SIGUSR2,&amp;act,NULL); <BR><BR><BR>for(;;){ <BR><BR>sleep(3); <BR><BR>} <BR><BR>} <BR><BR><BR>编译: <BR><BR><BR>gcc-oex1lock.c <BR><BR><BR>执行 <BR><BR><BR>./ex1 <BR><BR><BR>daemononduty! <BR><BR><BR>送信号 <BR><BR><BR>我们先找出该守护神程式的pid <BR><BR>PID=`cat/var/run/lock.pid` <BR><BR>接下来利用kill来送信号 <BR><BR><BR>kill$PID <BR><BR><BR>Receivesignal15 <BR><BR><BR>程式将会结束，并且/var/run/lock.pid将会被删除掉，以便下一次daemon再启动。注意到如果quit函数内，没有放exit()，程式将永远杀不掉。 <BR><BR><BR>接下来送一些其它的信号试试看。 <BR><BR>./ex1 <BR><BR>PID=`cat/var/run/lock.pid` <BR><BR>kill-HUP$PID <BR><BR><BR>Receivesignal1 <BR><BR><BR>您可以自行试试 <BR><BR>kill-INT$PID <BR><BR>kill-QUIT$PID <BR><BR>kill-ILL$PID <BR><BR>. <BR><BR>. <BR><BR>. <BR><BR>等等这些信号，看看他们的结果如何。 <BR><BR><BR>信号的定义 <BR><BR><BR>在/usr/include/signum.h中有各种信号的定义 <BR><BR>#defineSIGHUP1/*Hangup(POSIX).*/ <BR><BR>#defineSIGINT2/*Interrupt(ANSI).*/ <BR><BR>#defineSIGQUIT3/*Quit(POSIX).*/ <BR><BR>#defineSIGILL4/*Illegalinstruction(ANSI).*/ <BR><BR>#defineSIGTRAP5/*Tracetrap(POSIX).*/ <BR><BR>#defineSIGABRT6/*Abort(ANSI).*/ <BR><BR>#defineSIGIOT6/*IOTtrap(4.2BSD).*/ <BR><BR>#defineSIGBUS7/*BUSerror(4.2BSD).*/ <BR><BR>#defineSIGFPE8/*Floating-pointexception(ANSI). <BR><BR>*/ <BR><BR>#defineSIGKILL9/*Kill,unblockable(POSIX).*/ <BR><BR>#defineSIGUSR110/*User-definedsignal1(POSIX).*/ <BR><BR><BR>#defineSIGSEGV11/*Segmentationviolation(ANSI).*/ <BR><BR><BR>#defineSIGUSR212/*User-definedsignal2(POSIX).*/ <BR><BR><BR>#defineSIGPIPE13/*Brokenpipe(POSIX).*/ <BR><BR>#defineSIGALRM14/*Alarmclock(POSIX).*/ <BR><BR>#defineSIGTERM15/*Termination(ANSI).*/ <BR><BR>#defineSIGSTKFLT16/*???*/ <BR><BR>#defineSIGCLDSIGCHLD/*SameasSIGCHLD(SystemV).*/ <BR><BR>#defineSIGCHLD17/*Childstatushaschanged(POSIX). <BR><BR>*/ <BR><BR>#defineSIGCONT18/*Continue(POSIX).*/ <BR><BR>#defineSIGSTOP19/*Stop,unblockable(POSIX).*/ <BR><BR>#defineSIGTSTP20/*Keyboardstop(POSIX).*/ <BR><BR>#defineSIGTTIN21/*Backgroundreadfromtty(POSIX). <BR><BR>*/ <BR><BR>#defineSIGTTOU22/*Backgroundwritetotty(POSIX). <BR><BR>*/ <BR><BR>#defineSIGURG23/*Urgentconditiononsocket(4.2 <BR><BR>BSD).*/ <BR><BR>#defineSIGXCPU24/*CPUlimitexceeded(4.2BSD).*/ <BR><BR>#defineSIGXFSZ25/*Filesizelimitexceeded(4.2 <BR><BR>BSD).*/ <BR><BR>#defineSIGVTALRM26/*Virtualalarmclock(4.2BSD).*/ <BR><BR><BR>#defineSIGPROF27/*Profilingalarmclock(4.2BSD). <BR><BR>*/ <BR><BR>#defineSIGWINCH28/*Windowsizechange(4.3BSD,Sun). <BR><BR>*/ <BR><BR>#defineSIGPOLLSIGIO/*Pollableeventoccurred(System <BR><BR>V).*/ <BR><BR>#defineSIGIO29/*I/Onowpossible(4.2BSD).*/ <BR><BR>#defineSIGPWR30/*Powerfailurerestart(SystemV). <BR><BR>*/ <BR><BR>#defineSIGUNUSED31 <BR><BR><BR>函数宣告: <BR><BR><BR>SignalOperators <BR><BR><BR>intsigemptyset(sigset_t*set); <BR><BR>intsigfillset(sigset_t*set); <BR><BR>intsigaddset(sigset_t*set,intsignum); <BR><BR>intsigdelset(sigset_t*set,intsignum); <BR><BR>intsigismember(constsigset_t*set,intsignum); <BR><BR><BR>SignalHandlingFunctions <BR><BR><BR>intsigaction(intsignum,conststructsigaction*act,struct <BR><BR>sigaction*oldact); <BR><BR>intsigprocmask(inthow,constsigset_t*set,sigset_t <BR><BR>*oldset); <BR><BR>intsigpending(sigset_t*set); <BR><BR>intsigsuspend(constsigset_t*mask); <BR><BR><BR>StructureSignalAction <BR><BR>structsigaction{ <BR><BR>void(*sa_handler)(int); <BR><BR>sigset_tsa_mask; <BR><BR>intsa_flags; <BR><BR>void(*sa_restorer)(void); <BR><BR>} <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/494.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:46 <a href="http://www.cnitblog.com/drizztzou/articles/494.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>gcc中的内嵌汇编语言</title><link>http://www.cnitblog.com/drizztzou/articles/493.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:42:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/493.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/493.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/493.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/493.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/493.html</trackback:ping><description><![CDATA[gcc采用的是AT&amp;T的汇编格式,MS采用Intel的格式． <BR><BR>一　基本语法 <BR><BR>语法上主要有以下几个不同. <BR><BR>★ 寄存器命名原则 <BR>AT&amp;T: %eax Intel: eax <BR><BR>★源/目的操作数顺序 <BR>AT&amp;T: movl %eax,%ebx Intel: mov ebx,eax <BR><BR>★常数/立即数的格式 <BR>AT&amp;T: movl $_value,%ebx Intel: mov eax,_value <BR>把_value的地址放入eax寄存器 <BR><BR>AT&amp;T: movl $0xd00d,%ebx Intel: mov ebx,0xd00d <BR><BR>★ 操作数长度标识 <BR>AT&amp;T: movw %ax,%bx Intel: mov bx,ax <BR><BR>★寻址方式 <BR>AT&amp;T: immed32(basepointer,indexpointer,indexscale) <BR>Intel: [basepointer + indexpointer*indexscale + imm32) <BR>Linux工作于保护模式下，用的是３２位线性地址，所以在计算地址时 <BR>不用考虑segment:offset的问题．上式中的地址应为： <BR>imm32 + basepointer + indexpointer*indexscale <BR><BR>下面是一些例子： <BR>★直接寻址 <BR>AT&amp;T: _booga　; _booga是一个全局的C变量 <BR>注意加上$是表示地址引用，不加是表示值引用． <BR>注：对于局部变量，可以通过堆栈指针引用． <BR><BR>Intel: [_booga] <BR><BR>★寄存器间接寻址 <BR>AT&amp;T: (%eax) <BR>Intel: [eax] <BR><BR>★变址寻址 <BR>AT&amp;T: _variable(%eax) <BR>Intel: [eax + _variable] <BR><BR>AT&amp;T: _array(,%eax,4) <BR>Intel: [eax*4 + _array] <BR>AT&amp;T: _array(%ebx,%eax,8) <BR>Intel: [ebx + eax*8 + _array] <BR><BR><BR>二　基本的行内汇编 <BR><BR>基本的行内汇编很简单，一般是按照下面的格式 <BR>asm("statements"); <BR>例如：asm("nop"); asm("cli"); <BR>asm　和　__asm__是完全一样的． <BR>如果有多行汇编，则每一行都要加上　"\n\t" <BR>例如： <BR>asm( "pushl %eax\n\t" <BR>"movl $0,%eax\n\t" <BR>"popl %eax"); <BR>实际上gcc在处理汇编时，是要把asm(...)的内容"打印"到汇编 <BR>文件中，所以格式控制字符是必要的． <BR><BR>再例如： <BR>asm("movl %eax,%ebx"); <BR>asm("xorl %ebx,%edx"); <BR>asm("movl $0,_booga); <BR><BR>在上面的例子中，由于我们在行内汇编中改变了edx和ebx的值，但是 <BR>由于gcc的特殊的处理方法，即先形成汇编文件，再交给GAS去汇编， <BR>所以GAS并不知道我们已经改变了edx和ebx的值，如果程序的上下文 <BR>需要edx或ebx作暂存，这样就会引起严重的后果．对于变量_booga也 <BR>存在一样的问题．为了解决这个问题，就要用到扩展的行内汇编语法． <BR>三　扩展的行内汇编 <BR><BR>扩展的行内汇编类似于Watcom. <BR><BR>基本的格式是： <BR>asm ( "statements" : output_regs : input_regs : clobbered_regs); <BR><BR>clobbered_regs指的是被改变的寄存器． <BR>下面是一个例子(为方便起见，我使用全局变量）： <BR>int count=1; <BR>int value=1; <BR>int buf[10]; <BR>void main() <BR>{ <BR>asm( <BR>"cld \n\t" <BR>"rep \n\t" <BR>"stosl" <BR>: <BR>: "c" (count), "a" (value) , "D" (buf[0]) <BR>: "%ecx","%edi" ); <BR>} <BR>得到的主要汇编代码为： <BR>movl count,%ecx <BR>movl value,%eax <BR>movl buf,%edi <BR>#APP <BR>cld <BR>rep <BR>stosl <BR>#NO_APP <BR>cld,rep,stos就不用多解释了． <BR>这几条语句的功能是向buf中写上count个value值． <BR>冒号后的语句指明输入，输出和被改变的寄存器． <BR>通过冒号以后的语句，编译器就知道你的指令需要和改变哪些寄存器， <BR>从而可以优化寄存器的分配． <BR><BR>其中符号"c"(count)指示要把count的值放入ecx寄存器 <BR>类似的还有： <BR>a eax <BR>b ebx <BR>c ecx <BR>d edx <BR>S esi <BR>D edi <BR>I 常数值，(0 - 31) <BR>q,r 动态分配的寄存器 <BR>g eax,ebx,ecx,edx或内存变量 <BR>A 把eax和edx合成一个64位的寄存器(use long longs) <BR><BR>我们也可以让gcc自己选择合适的寄存器． <BR>如下面的例子： <BR>asm("leal (%1,%1,4),%0" <BR>: "=r" (x) <BR>: "0" (x) ); <BR>这段代码实现5*x的快速乘法． <BR>得到的主要汇编代码为： <BR>movl x,%eax <BR>#APP <BR>leal (%eax,%eax,4),%eax <BR>#NO_APP <BR>movl %eax,x <BR>几点说明： <BR>1.使用q指示编译器从eax,ebx,ecx,edx分配寄存器． <BR>使用r指示编译器从eax,ebx,ecx,edx,esi,edi分配寄存器． <BR>2.我们不必把编译器分配的寄存器放入改变的寄存器列表，因为寄存器 <BR>已经记住了它们． <BR>3."="是标示输出寄存器，必须这样用． <BR>4.数字%n的用法： <BR>数字表示的寄存器是按照出现和从左到右的顺序映射到用"r"或"q"请求 <BR>的寄存器．如果我们要重用"r"或"q"请求的寄存器的话，就可以使用它们． <BR>5.如果强制使用固定的寄存器的话，如不用%1,而用ebx,则 <BR>asm("leal (%%ebx,%%ebx,4),%0" <BR>: "=r" (x) <BR>: "0" (x) ); <BR>注意要使用两个%,因为一个%的语法已经被%n用掉了． <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/493.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:42 <a href="http://www.cnitblog.com/drizztzou/articles/493.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Socket编程基础</title><link>http://www.cnitblog.com/drizztzou/articles/492.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:41:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/492.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/492.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/492.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/492.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/492.html</trackback:ping><description><![CDATA[Socket编程基础本章以Berkeley Socket为主，主要介绍网络编程时常用的调用和程序使用它们的方法及基本结构。网络编程有两种主要的编程接口，一种是Berkeley UNIX（BSD UNIX）的socket编程接口，另一种是AT&amp;T的TLI接口（用于UNIXSYSV）。计算机网络的发展归功于DoD（U.S. Department of Defense）赞助研究的ARPANET网络。随着网络的发展而成熟的网络参考模型是TCP/IP参考模型，我们将在本章介绍该模型。八十年代早期，APRP在加利福尼亚大学的伯克利分校将TCP/IP第一次实现在UNIX系统上，这就是广为人知的socket接口。在UNIX System V R4.0中增加了socket接口和DARPA协议的支持程序作为与BSDUNIX统一的部分。所以，本书只介绍socket接口，不再介绍TLI接口。而且由于Internet越来越广泛的普及，所以这一章的重点将基于TCP/IP，对于socket虽然可以使用在UNIX域、XNS域中，但这里对于其他通信域不作过多的介绍。在进行本章阅读之前，我们希望读者对网络的基本知识有所了解，这样才能有利于理解和编程。读者还应该了解Internet网络的概念性知识，要了解TCP（传输控制协议）、UDP（用户数据报协议）、ICMP（网间控制报文协议）和IP（Internet协议族）等的作用。我们推荐Andrew S. Tanenbaum的《Computer Networks》第三版作为对网络概念学习的参考书。 <BR><BR>1 TCP/IP 基础知识 <BR>这里先假定读者对ISO的OSI七层模型已有了一定的了解，下面我们来看看TCP/IP模型。ISO的OSI对服务、接口和协议的概念区别十分明了，但它却没有真正的用户群。TCP/IP模型对服务、接口和协议的概念区别不象OSI模型那样明晰，但很实用。TCP/IP模型分为四层，对应于OSI七层模型如下图所示：图6-1 TCP/IP参考模型与OSI模型的近似对应关系在TCP/IP模型中，互联网层是基于无连接互联网络层的分组交换网络。在这一层中主机可以把报文（Packet）发往任何网络，报文独立地传向目标。互联网层定义了报文的格式和协议，这就是IP协议族（Internet Protocol）。互联网层的功能是将报文发送到目的地，主要的设计问题是报文路由和避免阻塞。互联网层上面是传输层，该层的主要功能和OSI模型的该层一样，主要使源和目的主机之间可以进行会话。该层定义了两个端到端的协议，一个是面向连接的传输控制协议TCP，另一个是无连接的用户数据报协议UDP。TCP/IP协议模型中没有会话层和表示层。传输层之上是应用层，它包含所有的高层协议，如远程虚拟终端协议TELNET、文件传输协议FTP、简单邮件传输协议SMTP等。这些高层协议中常见的如TELNET协议，用来允许用户远程登录到另一台UNIX机器；FTP协议用来传输文件，常见的有WU-FTP（Washington University的FTP服务器端程序，是一个免费程序）；SMTP协议用来传送email，常见的服务器端程序有netscape等公司制作的程序，也有免费使用的sendmail程序；还有域名系统服务DNS协议，新闻组传送协议NNTP，用于WWW的超文本传输协议HTTP等。主机到网络这一层，在TCP/IP模型中没有详细定义，这里不作介绍。如需要学习更多的网络知识及TCP/IP的详细描述，请参考专门的书籍，这里不再深入探讨。 <BR><BR>2 Socket一般描述 <BR>由于越来越多的计算机厂商，特别是工作站制造商如Sun等公司采用了Berkeley UNIX，socket接口被广泛采用，以至于现在，socket接口被广泛认可并成为了事实上的工业标准。目前的SYSV、BSD、OSF都将socket接口作为系统的一部分。当时设计如何支持TCP/IP协议时，有两种加入函数的方法，一种是直接加入支持TCP/IP协议的调用，另一种是加入支持一般网络协议的函数，而用参数来指定支持TCP/IP协议。Berkeley采用了后者，这样可以支持多协议族，TCP/IP是协议族之一（PF_INET）。 <BR><BR>2.1 socket 描述符 <BR>前面已经提到过，在UNIX中，进程要对文件进行操作，一般使用open调用打开一个文件进行访问，每个进程都有一个文件描述符表，该表中存放打开的文件描述符。用户使用open等调用得到的文件描述符其实是文件描述符在该表中的索引号，该表项的内容是一个指向文件表的指针。应用程序只要使用该描述符就可以对指定文件进行操作。同样，socket接口增加了网络通信操作的抽象定义，与文件操作一样，每个打开的socket都对应一个整数，我们称它为socket描述符，该整数也是socket描述符在文件描述符表中的索引值。但socket描述符在描述符表中的表项并不指向文件表，而是指向一个与该socket有关的数据结构。BSD UNIX中新增加了一个socket调用，应用程序可以调用它来新建一个socket描述符，注意进程用open只能产生文件描述符，而不能产生socket描述符。socket调用只能完成建立通信的部分工作，一旦建立了一个socket，应用程序可以使用其他特定的调用来为它添加其他详细信息，以完成建立通信的过程。 <BR><BR>2.2 从概念上理解socket的使用网络编程中最常见的是客户/服务器模式。以该模式编程时，服务端有一个进程（或多个进程）在指定的端口等待客户来连接，服务程序等待客户的连接信息，一旦连接上之后，就可以按设计的数据交换方法和格式进行数据传输。客户端在需要的时刻发出向服务端的连接请求。下面讲述中所用到的调用，将在下一节进行详细的阐述，这里为了便于理解，提到了这些调用及其大致的功能。使用socket调用后，仅产生了一个可以使用的socket描述符，这时还不能进行通信，还要使用其他的调用，以使得socket所指的结构中使用的信息被填写完。在使用TCP协议时，一般服务端进程先使用socket调用得到一个描述符，然后使用bind调用将一个名字与socket描述符连接起来，对于Internet域就是将Internet地址联编到socket。之后，服务端使用listen调用指出等待服务请求队列的长度。然后就可以使用accept调用等待客户端发起连接（一般是阻塞等待连接，后面章节会讲到非阻塞的方式），一旦有客户端发出连接，accept返回客户的地址信息，并返回一个新的socket描述符，该描述符与原先的socket有相同的特性，这时服务端就可以使用这个新的socket进行读写操作了。一般服务端可能在accept返回后创建一个新的进程进行与客户的通信，父进程则再到accept调用处等待另一个连接。客户端进程一般先使用socket调用得到一个socket描述符，然后使用connect向指定的服务器上的指定端口发起连接，一旦连接成功返回，就说明已经建立了与服务器的连接，这时就可以通过socket描述符进行读写操作了。下面是在客户和服务端使用TCP时，客户进程和服务进程使用系统调用的该程。 <BR>使用TCP的客户和服务端使用系统调用的图示使用无连接的UDP协议时，服务端进程创建一个socket，之后调用recvfrom接收客户端的数据报，然后调用sendto将要返回客户端的消息发送给客户进程。客户端也要先创建一个socket，再使用sendto向服务端进程发出请求，使用recvfrom得到返回的消息。 <BR>下面我们先编写一个非常简单的套接口客户端程序client,这个程序较为简单,它演示了一个无名的套接口连接, <BR>以及如何与一个服务器套接口连接,假设服务器套接口的名字是色server_socket. <BR>/* <BR>client.c <BR>*/ <BR>#include &lt;sys/types.h&gt; <BR>#include &lt;sys/socket.h&gt; <BR>#include &lt;stdio.h&gt; <BR>#include &lt;sys/un.h&gt; <BR>#include &lt;unistd.h&gt; <BR>int main() <BR>{ <BR>int sockfd; <BR>int len; <BR>struct sockaddr_un address; <BR>int result; <BR>char ch=A; <BR>sockfd=socket(AF_UNIX,SOCK_STREAM,0); <BR>/*以上建立客户端的套接口,采用AF_UNIX的unix域协议*/ <BR>address.sun_family=AF_UNIX; <BR>strcpy(address.sun_path,"server_socket"); <BR>len=sizeof(address); <BR>/*以上创建服务器套接口的地址,其中包括套接口类型,名称*/ <BR>result=connect(sockfd,(struct sockaddr *)&amp;address,len); <BR><BR>if(result==-1){ <BR>perror("oops:client1"); <BR>exit(1); <BR><BR>} <BR>/*以上我们试图与服务器套接口建立连接*/ <BR>write(sockfd,&amp;ch,1); <BR>read(sockfd,&amp;ch,1); <BR>/*如果成功,将向服务器端发送一个字符,然后读取服务器的回答*/ <BR><BR>printf("char from server=%c\n",ch); <BR>close(sockfd); <BR>exit(0); <BR>} <BR><BR>/* <BR>server.c <BR>*/ <BR>#include &lt;sys/types.h&gt; <BR>#include &lt;sys/socket.h&gt; <BR>#include &lt;stdio.h&gt; <BR>#include &lt;sys/un.h&gt; <BR>#include &lt;unistd.h&gt; <BR>int main() <BR>{ <BR>int server_sockfd,client_sockfd; <BR>int server_len,client_len; <BR>struct sockaddr_un server_address; <BR>struct sockaddr_un client_address; <BR>unlink("server_socket"); <BR>/*如果存在同名的套接口,则先删除*/ <BR>server_sockfd=socket(AF_UNIX,SOCK_STREAM,0); <BR>/*以上建立套接口,这时候无名*/ <BR>server_address.sun_family=AF_UNIX; <BR>strcpy(server_address.sun_path,"server_socket"); <BR>server_len=sizeof(server_address); <BR>bind(server_sockfd,(struct sockaddr *)&amp;server_address,server_len); <BR><BR>listen(server_sockfd,5); <BR>/*以上创建监听队列.等待用户的连接请求*/ <BR>while(1) <BR>{ <BR>char ch; <BR>printf("server waiting\n"); <BR>client_sockfd=accept(server_sockfd,(struct sockaddr *)&amp;client_address,&amp;client_len); <BR>/*以上接受一个客户的请求*/ <BR>read(client_sockfd,&amp;ch,1); <BR>/*因为连接一旦建立,客户就会先发消息过来,所以服务器先读*/ <BR>ch++; <BR>write(client_sockfd,&amp;ch,1); <BR>/*把读取的字符串做简单处理,回送*/ <BR>close(client_sockfd); <BR>} <BR>} <BR><BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/492.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:41 <a href="http://www.cnitblog.com/drizztzou/articles/492.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>使用 GDB 调试 Linux 软件</title><link>http://www.cnitblog.com/drizztzou/articles/491.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:39:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/491.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/491.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/491.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/491.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/491.html</trackback:ping><description><![CDATA[Linux 的大部分特色源自于 shell 的 GNU 调试器，也称作 gdb。gdb 可以让您查看程序的内部结构、打印变量值、设置断点，以及单步调试源代码。它是功能极其强大的工具，适用于修复程序代码中的问题。在本文中，我将尝试说明 gdb 有多棒，多实用。 <BR><BR>编译 <BR>开始调试之前，必须用程序中的调试信息编译要调试的程序。这样，gdb 才能够调试所使用的变量、代码行和函数。如果要进行编译，请在 gcc（或 g++）下使用额外的 -g 选项来编译程序： <BR><BR>gcc -g eg.c -o eg <BR><BR>运行 gdb <BR>在 shell 中，可以使用 gdb 命令并指定程序名作为参数来运行 gdb，例如 gdb eg；或者在 gdb 中，可以使用 file 命令来装入要调试的程序，例如 file eg。这两种方式都假设您是在包含程序的目录中执行命令。装入程序之后，可以用 gdb 命令 un 来启动程序。 <BR><BR>调试会话示例 <BR>如果一切正常，程序将执行到结束，此时 gdb 将重新获得控制。但如果有错误将会怎么样？这种情况下，gdb 会获得控制并中断程序，从而可以让您检查所有事物的状态，如果运气好的话，可以找出原因。为了引发这种情况，我们将使用一个示例程序： <BR><BR>代码示例 eg1.c <BR>#include <BR><BR>int wib(int no1, int no2) <BR>{ <BR>int result, diff; <BR>diff = no1 - no2; <BR>result = no1 / diff; <BR>return result; <BR>} <BR><BR>int main(int argc, char *argv[]) <BR>{ <BR>int value, div, result, i, total; <BR><BR>value = 10; <BR>div = 6; <BR>total = 0; <BR><BR>for(i = 0; i &lt; 10; i++) <BR>{ <BR>result = wib(value, div); <BR>total += result; <BR>div++; <BR>value--; <BR>} <BR><BR>printf("%d wibed by %d equals %dn", value, div, total); <BR>return 0; <BR>} <BR><BR><BR><BR><BR>这个程序将运行 10 次 for 循环，使用 wib() 函数计算出累积值，最后打印出结果。 <BR><BR>在您喜欢的文本编辑器中输入这个程序（要保持相同的行距），保存为 eg1.c，使用 gcc -g eg1.c -o eg1 进行编译，并用 gdb eg1 启动 gdb。使用 un 运行程序可能会产生以下消息： <BR><BR><BR>Program received signal SIGFPE, Arithmetic exception. <BR>0x80483ea in wib (no1=8, no2=8) at eg1.c:7 <BR>7 result = no1 / diff; <BR>(gdb) <BR><BR><BR><BR>gdb 指出在程序第 7 行发生一个算术异常，通常它会打印这一行以及 wib() 函数的自变量值。要查看第 7 行前后的源代码，请使用 list 命令，它通常会打印 10 行。再次输入 list（或者按回车重复上一条命令）将列出程序的下 10 行。从 gdb 消息中可以看出，第 7 行中的除法运算出了错，程序在这一行中将变量 "no1" 除以 "diff"。 <BR><BR>要查看变量的值，使用 gdb print 命令并指定变量名。输入 print no1 和 print diff，可以相应看到 "no1" 和 "diff" 的值，结果如下： <BR><BR><BR>(gdb) print no1 <BR>$5 = 8 <BR>(gdb) print diff <BR>$2 = 0 <BR><BR><BR><BR>gdb 指出 "no1" 等于 8，"diff" 等于 0。根据这些值和第 7 行中的语句，我们可以推断出算术异常是由除数为 0 的除法运算造成的。清单显示了第 6 行计算的变量 "diff"，我们可以打印 "diff" 表达式（使用 print no1 - no2 命令），来重新估计这个变量。gdb 告诉我们 wib 函数的这两个自变量都等于 8，于是我们要检查调用 wib() 函数的 main() 函数，以查看这是在什么时候发生的。在允许程序自然终止的同时，我们使用 continue 命令告诉 gdb 继续执行。 <BR><BR><BR>(gdb) continue <BR>Continuing. <BR><BR>Program terminated with signal SIGFPE, Arithmetic exception. <BR>The program no longer exists. <BR><BR><BR><BR>使用断点 <BR>为了查看在 main() 中发生了什么情况，可以在程序代码中的某一特定行或函数中设置断点，这样 gdb 会在遇到断点时中断执行。可以使用命令 reak main 在进入 main() 函数时设置断点，或者可以指定其它任何感兴趣的函数名来设置断点。然而，我们只希望在调用 wib() 函数之前中断执行。输入 list main 将打印从 main() 函数开始的源码清单，再次按回车将显示第 21 行上的 wib() 函数调用。要在那一行上设置断点，只需输入 reak 21。gdb 将发出以下响应： <BR><BR><BR>(gdb) break 21 <BR>Breakpoint 1 at 0x8048428: file eg1.c, line 21. <BR><BR><BR><BR>以显示它已在我们请求的行上设置了 1 号断点。 un 命令将从头重新运行程序，直到 gdb 中断为止。发生这种情况时，gdb 会生成一条消息，指出它在哪个断点上中断，以及程序运行到何处： <BR><BR><BR>Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:21 <BR>21 result = wib(value, div); <BR><BR><BR><BR>发出 print value 和 print div 将会显示在第一次调用 wib() 时，变量分别等于 10 和 6，而 print i 将会显示 0。幸好，gdb 将显示所有局部变量的值，并使用 info locals 命令保存大量输入信息。 <BR><BR>从以上的调查中可以看出，当 "value" 和 "div" 相等时就会出现问题，因此输入 continue 继续执行，直到下一次遇到 1 号断点。对于这次迭代，info locals 显示了 value=9 和 div=7。 <BR><BR>与其再次继续，还不如使用 ext 命令单步调试程序，以查看 "value" 和 "div" 是如何改变的。gdb 将响应： <BR><BR><BR>(gdb) next <BR>22 total += result; <BR><BR><BR><BR>再按两次回车将显示加法和减法表达式： <BR><BR><BR>(gdb) <BR>23 div++; <BR>(gdb) <BR>24 value--; <BR><BR><BR><BR>再按两次回车将显示第 21 行，wib() 调用。info locals 将显示目前 "div" 等于 "value"，这就意味着将发生问题。如果有兴趣，可以使用 step 命令（与 ext 形成对比， ext 将跳过函数调用）来继续执行 wib() 函数，以再次查看除法错误，然后使用 ext 来计算 "result"。 <BR><BR>现在已完成了调试，可以使用 quit 命令退出 gdb。由于程序仍在运行，这个操作会终止它，gdb 将提示您确认。 <BR><BR>更多断点和观察点 <BR>由于我们想要知道在调用 wib() 函数之前 "value" 什么时候等于 "div"，因此在上一示例中我们在第 21 行中设置断点。我们必须继续执行两次程序才会发生这种情况，但是只要在断点上设置一个条件就可以使 gdb 只在 "value" 与 "div" 真正相等时暂停。要设置条件，可以在定义断点时指定 "break <LINE number>if <CONDITIONAL expression>"。将 eg1 再次装入 gdb，并输入： <BR><BR><BR>(gdb) break 21 if value==div <BR>Breakpoint 1 at 0x8048428: file eg1.c, line 21. <BR><BR><BR><BR>如果已经在第 21 行中设置了断点，如 1 号断点，则可以使用 condition 命令来代替在断点上设置条件： <BR><BR><BR>(gdb) condition 1 value==div <BR><BR><BR><BR>使用 un 运行 eg1.c 时，如果 "value" 等于 "div"，gdb 将中断，从而避免了在它们相等之前必须手工执行 continue。调试 C 程序时，断点条件可以是任何有效的 C 表达式，一定要是程序所使用语言的任意有效表达式。条件中指定的变量必须在设置了断点的行中，否则表达式就没有什么意义！ <BR><BR>使用 condition 命令时，如果指定断点编号但又不指定表达式，可以将断点设置成无条件断点，例如，condition 1 就将 1 号断点设置成无条件断点。 <BR><BR>要查看当前定义了什么断点及其条件，请发出命令 info break： <BR><BR><BR>(gdb) info break <BR>Num Type Disp Enb Address What <BR>1 breakpoint keep y 0x08048428 in main at eg1.c:21 <BR>stop only if value == div <BR>breakpoint already hit 1 time <BR><BR><BR><BR>除了所有条件和已经遇到断点多少次之外，断点信息还在 Enb 列中指定了是否启用该断点。可以使用命令 disable <BREAKPOINT number>、enable <BREAKPOINT number>或 delete <BREAKPOINT number>来禁用、启用和彻底删除断点，例如 disable 1 将阻止在 1 号断点处中断。 <BR><BR>如果我们对 "value" 什么时候变得与 "div" 相等更感兴趣，那么可以使用另一种断点，称作监视。当指定表达式的值改变时，监视点将中断程序执行，但必须在表达式中所使用的变量在作用域中时设置监视点。要获取作用域中的 "value" 和 "div"，可以在 main 函数上设置断点，然后运行程序，当遇到 main() 断点时设置监视点。重新启动 gdb，并装入 eg1，然后输入： <BR><BR><BR>(gdb) break main <BR>Breakpoint 1 at 0x8048402: file eg1.c, line 15. <BR>(gdb) run <BR>... <BR>Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:15 <BR>15 value = 10; <BR><BR><BR><BR>要了解 "div" 何时更改，可以使用 watch div，但由于要在 "div" 等于 "value" 时中断，那么应输入： <BR><BR><BR>(gdb) watch div==value <BR>Hardware watchpoint 2: div == value <BR><BR><BR><BR>如果继续执行，那么当表达式 "div==value" 的值从 0（假）变成 1（真）时，gdb 将中断： <BR><BR><BR>(gdb) continue <BR>Continuing. <BR>Hardware watchpoint 2: div == value <BR><BR>Old value = 0 <BR>New value = 1 <BR>main (argc=1, argv=0xbffff954) at eg1.c:19 <BR>19 for(i = 0; i &lt; 10; i++) <BR><BR><BR><BR>info locals 命令将验证 "value" 是否确实等于 "div"（再次声明，是 8）。 <BR><BR>info watch 命令将列出已定义的监视点和断点（此命令等价于 info break），而且可以使用与断点相同的语法来启用、禁用和删除监视点。 <BR><BR>core 文件 <BR>在 gdb 下运行程序可以使俘获错误变得更容易，但在调试器外运行的程序通常会中止而只留下一个 core 文件。gdb 可以装入 core 文件，并让您检查程序中止之前的状态。 <BR><BR>在 gdb 外运行示例程序 eg1 将会导致核心信息转储： <BR><BR>$ ./eg1 <BR>Floating point exception (core dumped) <BR><BR><BR><BR>要使用 core 文件启动 gdb，在 shell 中发出命令 gdb eg1 core 或 gdb eg1 -c core。gdb 将装入 core 文件，eg1 的程序清单，显示程序是如何终止的，并显示非常类似于我们刚才在 gdb 下运行程序时看到的消息： <BR><BR><BR>... <BR>Core was generated by `./eg1. <BR>Program terminated with signal 8, Floating point exception. <BR>... <BR>#0 0x80483ea in wib (no1=8, no2=8) at eg1.c:7 <BR>7 result = no1 / diff; <BR><BR><BR><BR>此时，可以发出 info locals、print、info args 和 list 命令来查看引起除数为零的值。info variables 命令将打印出所有程序变量的值，但这要进行很长时间，因为 gdb 将打印 C 库和程序代码中的变量。为了更容易地查明在调用 wib() 的函数中发生了什么情况，可以使用 gdb 的堆栈命令。 <BR><BR>堆栈跟踪 <BR>程序“调用堆栈”是当前函数之前的所有已调用函数的列表（包括当前函数）。每个函数及其变量都被分配了一个“帧”，最近调用的函数在 0 号帧中（“底部”帧）。要打印堆栈，发出命令 t（acktrace [回溯] 的缩写）： <BR><BR><BR>(gdb) bt <BR>#0 0x80483ea in wib (no1=8, no2=8) at eg1.c:7 <BR>#1 0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21 <BR><BR><BR><BR>此结果显示了在 main() 的第 21 行中调用了函数 wib()（只要使用 list 21 就能证实这一点），而且 wib() 在 0 号帧中，main() 在 1 号帧中。由于 wib() 在 0 号帧中，那么它就是执行程序时发生算术错误的函数。 <BR><BR>实际上，发出 info locals 命令时，gdb 会打印出当前帧中的局部变量，缺省情况下，这个帧中的函数就是被中断的函数（0 号帧）。可以使用命令 frame 打印当前帧。要查看 main 函数（在 1 号帧中）中的变量，可以发出 frame 1 切换到 1 号帧，然后发出 info locals 命令： <BR><BR><BR>(gdb) frame 1 <BR>#1 0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21 <BR>21 result = wib(value, div); <BR>(gdb) info locals <BR>value = 8 <BR>div = 8 <BR>result = 4 <BR>i = 2 <BR>total = 6 <BR><BR><BR><BR>此信息显示了在第三次执行 "for" 循环时（i 等于 2）发生了错误，此时 "value" 等于 "div"。 <BR><BR>可以通过如上所示在 frame 命令中明确指定号码，或者使用 up 命令在堆栈中上移以及 down 命令在堆栈中下移来切换帧。要获取有关帧的进一步信息，如它的地址和程序语言，可以使用命令 info frame。 <BR><BR>gdb 堆栈命令可以在程序执行期间使用，也可以在 core 文件中使用，因此对于复杂的程序，可以在程序运行时跟踪它是如何转到函数的。 <BR><BR>连接到其它进程 <BR>除了调试 core 文件或程序之外，gdb 还可以连接到已经运行的进程（它的程序已经过编译，并加入了调试信息），并中断该进程。只需用希望 gdb 连接的进程标识替换 core 文件名就可以执行此操作。以下是一个执行循环并睡眠的示例程序： <BR><BR>eg2 示例代码 <BR>#include <BR>int main(int argc, char *argv[]) <BR>{ <BR>int i; <BR>for(i = 0; i &lt; 60; i++) <BR>{ <BR>sleep(1); <BR>} <BR><BR>return 0; <BR>} <BR><BR><BR><BR><BR>使用 gcc -g eg2.c -o eg2 编译该程序并使用 ./eg2 &amp; 运行该程序。请留意在启动该程序时在背景上打印的进程标识，在本例中是 1283： <BR><BR>./eg2 &amp; <BR>[3] 1283 <BR><BR><BR><BR>启动 gdb 并指定进程标识，在我举的这个例子中是 gdb eg2 1283。gdb 会查找一个叫作 "1283" 的 core 文件。如果没有找到，那么只要进程 1283 正在运行（在本例中可能在 sleep() 中），gdb 就会连接并中断该进程： <BR><BR><BR>... <BR>/home/seager/gdb/1283: No such file or directory. <BR>Attaching to program: /home/seager/gdb/eg2, Pid 1283 <BR>... <BR>0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6 <BR>(gdb) <BR><BR><BR><BR>此时，可以发出所有常用 gdb 命令。可以使用 acktrace 来查看当前位置与 main() 的相对关系，以及 mian() 的帧号是什么，然后切换到 main() 所在的帧，查看已经在 "for" 循环中运行了多少次： <BR><BR>(gdb) backtrace <BR>#0 0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6 <BR>#1 0x400a877d in __sleep (seconds=1) at ../sysdeps/unix/sysv/linux/sleep.c:78 <BR>#2 0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7 <BR>(gdb) frame 2 <BR>#2 0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7 <BR>7 sleep(1); <BR>(gdb) print i <BR>$1 = 50 <BR><BR><BR><BR>如果已经完成了对程序的修改，可以 detach 命令继续执行程序，或者 kill 命令杀死进程。还可以首先使用 file eg2 装入文件，然后发出 attach 1283 命令连接到进程标识 1283 下的 eg2。 <BR><BR>其它小技巧 <BR>gdb 可以让您通过使用 shell 命令在不退出调试环境的情况下运行 shell 命令，调用形式是 shell [commandline]，这有助于在调试时更改源代码。 <BR><BR>最后，在程序运行时，可以使用 set 命令修改变量的值。在 gdb 下再次运行 eg1，使用命令 reak 7 if diff==0 在第 7 行（将在此处计算结果）设置条件断点，然后运行程序。当 gdb 中断执行时，可以将 "diff" 设置成非零值，使程序继续运行直至结束： <BR><BR><BR>Breakpoint 1, wib (no1=8, no2=8) at eg1.c:7 <BR>7 result = no1 / diff; <BR>(gdb) print diff <BR>$1 = 0 <BR>(gdb) set diff=1 <BR>(gdb) continue <BR>Continuing. <BR>0 wibed by 16 equals 10 <BR><BR>Program exited normally. <BR><BR><BR><BR>结束语 <BR>GNU 调试器是所有程序员工具库中的一个功能非常强大的工具。在本文中，我只介绍了 gdb 的一小部分功能。要了解更多知识，建议您阅读 GNU 调试器手册。 <BR><BR>参考资料 <BR><BR>GNU 调试器手册 <BR>调试会话示例的源代码。 <BR>连接示例的源代码。 <BR><BR>关于作者 <BR><BR>David Seager 是 IBM 的软件开发人员，他从事 Linux 和基于 Web 的应用工作已有两年时间了。<img src ="http://www.cnitblog.com/drizztzou/aggbug/491.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:39 <a href="http://www.cnitblog.com/drizztzou/articles/491.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>POSIX 线程详解</title><link>http://www.cnitblog.com/drizztzou/articles/490.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:38:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/490.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/490.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/490.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/490.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/490.html</trackback:ping><description><![CDATA[线程是有趣的 <BR>线程是快捷的 <BR>线程是可移植的 <BR>第一个线程 <BR>无父，无子 <BR>同步漫游 <BR>参考资料 <BR>关于作者 <BR><BR><BR><BR>POSIX（可移植操作系统接口）线程是提高代码响应和性能的有力手段。在本系列中，Daniel Robbins 向您精确地展示在编程中如何使用线程。其中还涉及大量幕后细节，读完本系列文章，您完全可以运用 POSIX 线程创建多线程程序。 <BR><BR>线程是有趣的 <BR>了解如何正确运用线程是每一个优秀程序员必备的素质。线程类似于进程。如同进程，线程由内核按时间分片进行管理。在单处理器系统中，内核使用时间分片来模拟线程的并发执行，这种方式和进程的相同。而在多处理器系统中，如同多个进程，线程实际上一样可以并发执行。 <BR><BR>那么为什么对于大多数合作性任务，多线程比多个独立的进程更优越呢？这是因为，线程共享相同的内存空间。不同的线程可以存取内存中的同一个变量。所以，程序中的所有线程都可以读或写声明过的全局变量。如果曾用 fork() 编写过重要代码，就会认识到这个工具的重要性。为什么呢？虽然 fork() 允许创建多个进程，但它还会带来以下通信问题: 如何让多个进程相互通信，这里每个进程都有各自独立的内存空间。对这个问题没有一个简单的答案。虽然有许多不同种类的本地 IPC (进程间通信），但它们都遇到两个重要障碍： <BR><BR><BR>强加了某种形式的额外内核开销，从而降低性能。 <BR>对于大多数情形，IPC 不是对于代码的“自然”扩展。通常极大地增加了程序的复杂性。 <BR><BR>双重坏事: 开销和复杂性都非好事。如果曾经为了支持 IPC 而对程序大动干戈过，那么您就会真正欣赏线程提供的简单共享内存机制。由于所有的线程都驻留在同一内存空间，POSIX 线程无需进行开销大而复杂的长距离调用。只要利用简单的同步机制，程序中所有的线程都可以读取和修改已有的数据结构。而无需将数据经由文件描述符转储或挤入紧窄的共享内存空间。仅此一个原因，就足以让您考虑应该采用单进程/多线程模式而非多进程/单线程模式。 <BR><BR>线程是快捷的 <BR>不仅如此。线程同样还是非常快捷的。与标准 fork() 相比，线程带来的开销很小。内核无需单独复制进程的内存空间或文件描述符等等。这就节省了大量的 CPU 时间，使得线程创建比新进程创建快上十到一百倍。因为这一点，可以大量使用线程而无需太过于担心带来的 CPU 或内存不足。使用 fork() 时导致的大量 CPU 占用也不复存在。这表示只要在程序中有意义，通常就可以创建线程。 <BR><BR>当然，和进程一样，线程将利用多 CPU。如果软件是针对多处理器系统设计的，这就真的是一大特性（如果软件是开放源码，则最终可能在不少平台上运行）。特定类型线程程序（尤其是 CPU 密集型程序）的性能将随系统中处理器的数目几乎线性地提高。如果正在编写 CPU 非常密集型的程序，则绝对想设法在代码中使用多线程。一旦掌握了线程编码，无需使用繁琐的 IPC 和其它复杂的通信机制，就能够以全新和创造性的方法解决编码难题。所有这些特性配合在一起使得多线程编程更有趣、快速和灵活。 <BR><BR>线程是可移植的 <BR>如果熟悉 Linux 编程，就有可能知道 __clone() 系统调用。__clone() 类似于 fork()，同时也有许多线程的特性。例如，使用 __clone()，新的子进程可以有选择地共享父进程的执行环境（内存空间，文件描述符等）。这是好的一面。但 __clone() 也有不足之处。正如__clone() 在线帮助指出： <BR><BR>“__clone 调用是特定于 Linux 平台的，不适用于实现可移植的程序。欲编写线程化应用程序（多线程控制同一内存空间），最好使用实现 POSIX 1003.1c 线程 API 的库，例如 Linux-Threads 库。参阅 pthread_create(3thr)。” <BR><BR>虽然 __clone() 有线程的许多特性，但它是不可移植的。当然这并不意味着代码中不能使用它。但在软件中考虑使用 __clone() 时应当权衡这一事实。值得庆幸的是，正如 __clone() 在线帮助指出，有一种更好的替代方案：POSIX 线程。如果想编写可移植的多线程代码，代码可运行于 Solaris、FreeBSD、Linux 和其它平台，POSIX 线程是一种当然之选。 <BR><BR>第一个线程 <BR>下面是一个 POSIX 线程的简单示例程序： <BR><BR>thread1.c <BR>#include &lt;pthread.h&gt; <BR>#include &lt;stdlib.h&gt; <BR>#include &lt;unistd.h&gt; <BR><BR>void *thread_function(void *arg) { <BR>int i; <BR>for ( i=0; i&lt;20; i++) { <BR>printf("Thread says hi!\n"); <BR>sleep(1); <BR>} <BR>return NULL; <BR>} <BR><BR>int main(void) { <BR><BR>pthread_t mythread; <BR><BR>if ( pthread_create( &amp;mythread, NULL, thread_function, NULL) ) { <BR>printf("error creating thread."); <BR>abort(); <BR>} <BR><BR>if ( pthread_join ( mythread, NULL ) ) { <BR>printf("error joining thread."); <BR>abort(); <BR>} <BR><BR>exit(0); <BR><BR>} <BR><BR><BR><BR><BR>要编译这个程序，只需先将程序存为 thread1.c，然后输入： <BR><BR>$ gcc thread1.c -o thread1 -lpthread <BR><BR><BR><BR><BR>运行则输入： <BR><BR>$ ./thread1 <BR><BR><BR><BR><BR>理解 thread1.c <BR>thread1.c 是一个非常简单的线程程序。虽然它没有实现什么有用的功能，但可以帮助理解线程的运行机制。下面，我们一步一步地了解这个程序是干什么的。main() 中声明了变量 mythread，类型是 pthread_t。pthread_t 类型在 pthread.h 中定义，通常称为“线程 id”（缩写为 "tid"）。可以认为它是一种线程句柄。 <BR><BR>mythread 声明后（记住 mythread 只是一个 "tid"，或是将要创建的线程的句柄），调用 pthread_create 函数创建一个真实活动的线程。不要因为 pthread_create() 在 "if" 语句内而受其迷惑。由于 pthread_create() 执行成功时返回零而失败时则返回非零值，将 pthread_create() 函数调用放在 if() 语句中只是为了方便地检测失败的调用。让我们查看一下 pthread_create 参数。第一个参数 &amp;mythread 是指向 mythread 的指针。第二个参数当前为 NULL，可用来定义线程的某些属性。由于缺省的线程属性是适用的，只需将该参数设为 NULL。 <BR><BR>第三个参数是新线程启动时调用的函数名。本例中，函数名为 thread_function()。当 thread_function() 返回时，新线程将终止。本例中，线程函数没有实现大的功能。它仅将 "Thread says hi!" 输出 20 次然后退出。注意 thread_function() 接受 void * 作为参数，同时返回值的类型也是 void *。这表明可以用 void * 向新线程传递任意类型的数据，新线程完成时也可返回任意类型的数据。那如何向线程传递一个任意参数？很简单。只要利用 pthread_create() 中的第四个参数。本例中，因为没有必要将任何数据传给微不足道的 thread_function()，所以将第四个参数设为 NULL。 <BR><BR>您也许已推测到，在 pthread_create() 成功返回之后，程序将包含两个线程。等一等，两个线程？我们不是只创建了一个线程吗？不错，我们只创建了一个进程。但是主程序同样也是一个线程。可以这样理解：如果编写的程序根本没有使用 POSIX 线程，则该程序是单线程的（这个单线程称为“主”线程）。创建一个新线程之后程序总共就有两个线程了。 <BR><BR>我想此时您至少有两个重要问题。第一个问题，新线程创建之后主线程如何运行。答案，主线程按顺序继续执行下一行程序（本例中执行 "if (pthread_join(...))"）。第二个问题，新线程结束时如何处理。答案，新线程先停止，然后作为其清理过程的一部分，等待与另一个线程合并或“连接”。 <BR><BR>现在，来看一下 pthread_join()。正如 pthread_create() 将一个线程拆分为两个， pthread_join() 将两个线程合并为一个线程。pthread_join() 的第一个参数是 tid mythread。第二个参数是指向 void 指针的指针。如果 void 指针不为 NULL，pthread_join 将线程的 void * 返回值放置在指定的位置上。由于我们不必理会 thread_function() 的返回值，所以将其设为 NULL. <BR><BR>您会注意到 thread_function() 花了 20 秒才完成。在 thread_function() 结束很久之前，主线程就已经调用了 pthread_join()。如果发生这种情况，主线程将中断（转向睡眠）然后等待 thread_function() 完成。当 thread_function() 完成后, pthread_join() 将返回。这时程序又只有一个主线程。当程序退出时，所有新线程已经使用 pthread_join() 合并了。这就是应该如何处理在程序中创建的每个新线程的过程。如果没有合并一个新线程，则它仍然对系统的最大线程数限制不利。这意味着如果未对线程做正确的清理，最终会导致 pthread_create() 调用失败。 <BR><BR>无父，无子 <BR>如果使用过 fork() 系统调用，可能熟悉父进程和子进程的概念。当用 fork() 创建另一个新进程时，新进程是子进程，原始进程是父进程。这创建了可能非常有用的层次关系，尤其是等待子进程终止时。例如，waitpid() 函数让当前进程等待所有子进程终止。waitpid() 用来在父进程中实现简单的清理过程。 <BR><BR>而 POSIX 线程就更有意思。您可能已经注意到我一直有意避免使用“父线程”和“子线程”的说法。这是因为 POSIX 线程中不存在这种层次关系。虽然主线程可以创建一个新线程，新线程可以创建另一个新线程，POSIX 线程标准将它们视为等同的层次。所以等待子线程退出的概念在这里没有意义。POSIX 线程标准不记录任何“家族”信息。缺少家族信息有一个主要含意：如果要等待一个线程终止，就必须将线程的 tid 传递给 pthread_join()。线程库无法为您断定 tid。 <BR><BR>对大多数开发者来说这不是个好消息，因为这会使有多个线程的程序复杂化。不过不要为此担忧。POSIX 线程标准提供了有效地管理多个线程所需要的所有工具。实际上，没有父/子关系这一事实却为在程序中使用线程开辟了更创造性的方法。例如，如果有一个线程称为线程 1，线程 1 创建了称为线程 2 的线程，则线程 1 自己没有必要调用 pthread_join() 来合并线程 2，程序中其它任一线程都可以做到。当编写大量使用线程的代码时，这就可能允许发生有趣的事情。例如，可以创建一个包含所有已停止线程的全局“死线程列表”，然后让一个专门的清理线程专等停止的线程加到列表中。这个清理线程调用 pthread_join() 将刚停止的线程与自己合并。现在，仅用一个线程就巧妙和有效地处理了全部清理。 <BR><BR>同步漫游 <BR>现在我们来看一些代码，这些代码做了一些意想不到的事情。thread2.c 的代码如下： <BR><BR>thread2.c <BR>#include &lt;pthread.h&gt; <BR>#include &lt;stdlib.h&gt; <BR>#include &lt;unistd.h&gt; <BR>#include &lt;stdio.h&gt; <BR><BR>int myglobal; <BR><BR>void *thread_function(void *arg) { <BR>int i,j; <BR>for ( i=0; i&lt;20; i++) { <BR>j=myglobal; <BR>j=j+1; <BR>printf("."); <BR>fflush(stdout); <BR>sleep(1); <BR>myglobal=j; <BR>} <BR>return NULL; <BR>} <BR><BR>int main(void) { <BR><BR>pthread_t mythread; <BR>int i; <BR><BR>if ( pthread_create( &amp;mythread, NULL, thread_function, NULL) ) { <BR>printf("error creating thread."); <BR>abort(); <BR>} <BR><BR>for ( i=0; i&lt;20; i++) { <BR>myglobal=myglobal+1; <BR>printf("o"); <BR>fflush(stdout); <BR>sleep(1); <BR>} <BR><BR>if ( pthread_join ( mythread, NULL ) ) { <BR>printf("error joining thread."); <BR>abort(); <BR>} <BR><BR>printf("\nmyglobal equals %d\n",myglobal); <BR><BR>exit(0); <BR><BR>} <BR><BR><BR><BR><BR>理解 thread2.c <BR>如同第一个程序，这个程序创建一个新线程。主线程和新线程都将全局变量 myglobal 加一 20 次。但是程序本身产生了某些意想不到的结果。编译代码请输入： <BR><BR>$ gcc thread2.c -o thread2 -lpthread <BR><BR><BR><BR><BR>运行请输入： <BR><BR>$ ./thread2 <BR><BR><BR><BR><BR>输出： <BR><BR>$ ./thread2 <BR>..o.o.o.o.oo.o.o.o.o.o.o.o.o.o..o.o.o.o.o <BR>myglobal equals 21 <BR><BR><BR><BR><BR>非常意外吧！因为 myglobal 从零开始，主线程和新线程各自对其进行了 20 次加一, 程序结束时 myglobal 值应当等于 40。由于 myglobal 输出结果为 21，这其中肯定有问题。但是究竟是什么呢？ <BR><BR>放弃吗？好，让我来解释是怎么一回事。首先查看函数 thread_function()。注意如何将 myglobal 复制到局部变量 "j" 了吗? 接着将 j 加一, 再睡眠一秒，然后到这时才将新的 j 值复制到 myglobal？这就是关键所在。设想一下，如果主线程就在新线程将 myglobal 值复制给 j 后立即将 myglobal 加一，会发生什么？当 thread_function() 将 j 的值写回 myglobal 时，就覆盖了主线程所做的修改。 <BR><BR>当编写线程程序时，应避免产生这种无用的副作用，否则只会浪费时间（当然，除了编写关于 POSIX 线程的文章时有用）。那么，如何才能排除这种问题呢？ <BR><BR>由于是将 myglobal 复制给 j 并且等了一秒之后才写回时产生问题，可以尝试避免使用临时局部变量并直接将 myglobal 加一。虽然这种解决方案对这个特定例子适用，但它还是不正确。如果我们对 myglobal 进行相对复杂的数学运算，而不是简单的加一，这种方法就会失效。但是为什么呢？ <BR><BR>要理解这个问题，必须记住线程是并发运行的。即使在单处理器系统上运行（内核利用时间分片模拟多任务）也是可以的，从程序员的角度，想像两个线程是同时执行的。thread2.c 出现问题是因为 thread_function() 依赖以下论据：在 myglobal 加一之前的大约一秒钟期间不会修改 myglobal。需要有些途径让一个线程在对 myglobal 做更改时通知其它线程“不要靠近”。我将在下一篇文章中讲解如何做到这一点。到时候见。 <BR><BR>第 2 部分 <BR>称作互斥对象的小玩意 <BR><BR>作者:Daniel Robbins <BR><BR>内容： <BR><BR><BR>互斥我吧！ <BR>解读一下 <BR>为什么要用互斥对象？ <BR>线程内幕 1 <BR>线程内幕 2 <BR>许多互斥对象 <BR>使用调用：初始化 <BR>使用调用：锁定 <BR>等待条件发生 <BR>参考资料 <BR>关于作者 <BR><BR><BR><BR>POSIX 线程是提高代码响应和性能的有力手段。在此三部分系列文章的第二篇中，Daniel Robbins 将说明，如何使用被称为互斥对象的灵巧小玩意，来保护线程代码中共享数据结构的完整性。 <BR><BR>互斥我吧！ <BR>在前一篇文章中，谈到了会导致异常结果的线程代码。两个线程分别对同一个全局变量进行了二十次加一。变量的值最后应该是 40，但最终值却是 21。这是怎么回事呢？因为一个线程不停地“取消”了另一个线程执行的加一操作，所以产生这个问题。现在让我们来查看改正后的代码，它使用互斥对象(mutex)来解决该问题： <BR><BR>thread3.c <BR>#include &lt;pthread.h&gt; <BR>#include &lt;stdlib.h&gt; <BR>#include &lt;unistd.h&gt; <BR>#include &lt;stdio.h&gt; <BR><BR>int myglobal; <BR>pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER; <BR><BR>void *thread_function(void *arg) { <BR>int i,j; <BR>for ( i=0; i&lt;20; i++) { <BR>pthread_mutex_lock(&amp;mymutex); <BR>j=myglobal; <BR>j=j+1; <BR>printf("."); <BR>fflush(stdout); <BR>sleep(1); <BR>myglobal=j; <BR>pthread_mutex_unlock(&amp;mymutex); <BR>} <BR>return NULL; <BR>} <BR><BR>int main(void) { <BR><BR>pthread_t mythread; <BR>int i; <BR><BR>if ( pthread_create( &amp;mythread, NULL, thread_function, NULL) ) { <BR>printf("error creating thread."); <BR>abort(); <BR>} <BR><BR>for ( i=0; i&lt;20; i++) { <BR>pthread_mutex_lock(&amp;mymutex); <BR>myglobal=myglobal+1; <BR>pthread_mutex_unlock(&amp;mymutex); <BR>printf("o"); <BR>fflush(stdout); <BR>sleep(1); <BR>} <BR><BR>if ( pthread_join ( mythread, NULL ) ) { <BR>printf("error joining thread."); <BR>abort(); <BR>} <BR><BR>printf("nmyglobal equals %dn",myglobal); <BR><BR>exit(0); <BR><BR>} <BR><BR><BR><BR>解读一下 <BR>如果将这段代码与前一篇文章中给出的版本作一个比较，就会注意到增加了 pthread_mutex_lock() 和 pthread_mutex_unlock() 函数调用。在线程程序中这些调用执行了不可或缺的功能。他们提供了一种相互排斥的方法（互斥对象即由此得名）。两个线程不能同时对同一个互斥对象加锁。 <BR><BR>互斥对象是这样工作的。如果线程 a 试图锁定一个互斥对象，而此时线程 b 已锁定了同一个互斥对象时，线程 a 就将进入睡眠状态。一旦线程 b 释放了互斥对象（通过 pthread_mutex_unlock() 调用），线程 a 就能够锁定这个互斥对象（换句话说，线程 a 就将从 pthread_mutex_lock() 函数调用中返回，同时互斥对象被锁定）。同样地，当线程 a 正锁定互斥对象时，如果线程 c 试图锁定互斥对象的话，线程 c 也将临时进入睡眠状态。对已锁定的互斥对象上调用 pthread_mutex_lock() 的所有线程都将进入睡眠状态，这些睡眠的线程将“排队”访问这个互斥对象。 <BR><BR>通常使用 pthread_mutex_lock() 和 pthread_mutex_unlock() 来保护数据结构。这就是说，通过线程的锁定和解锁，对于某一数据结构，确保某一时刻只能有一个线程能够访问它。可以推测到，当线程试图锁定一个未加锁的互斥对象时，POSIX 线程库将同意锁定，而不会使线程进入睡眠状态。 <BR><BR>请看这幅轻松的漫画，四个小精灵重现了最近一次 pthread_mutex_lock() 调用的一个场面。 <BR><BR><BR><BR>图中，锁定了互斥对象的线程能够存取复杂的数据结构，而不必担心同时会有其它线程干扰。那个数据结构实际上是“冻结”了，直到互斥对象被解锁为止。pthread_mutex_lock() 和 pthread_mutex_unlock() 函数调用，如同“在施工中”标志一样，将正在修改和读取的某一特定共享数据包围起来。这两个函数调用的作用就是警告其它线程，要它们继续睡眠并等待轮到它们对互斥对象加锁。当然，除非在每个对特定数据结构进行读写操作的语句前后，都分别放上 pthread_mutex_lock() 和 pthread_mutext_unlock() 调用，才会出现这种情况。 <BR><BR>为什么要用互斥对象？ <BR>听上去很有趣，但究竟为什么要让线程睡眠呢？要知道，线程的主要优点不就是其具有独立工作、更多的时候是同时工作的能力吗？是的，确实是这样。然而，每个重要的线程程序都需要使用某些互斥对象。让我们再看一下示例程序以便理解原因所在。 <BR><BR>请看 thread_function()，循环中一开始就锁定了互斥对象，最后才将它解锁。在这个示例程序中，mymutex 用来保护 myglobal 的值。仔细查看 thread_function()，加一代码把 myglobal 复制到一个局部变量，对局部变量加一，睡眠一秒钟，在这之后才把局部变量的值传回给 myglobal。不使用互斥对象时，即使主线程在 thread_function() 线程睡眠一秒钟期间内对 myglobal 加一，thread_function() 苏醒后也会覆盖主线程所加的值。使用互斥对象能够保证这种情形不会发生。（您也许会想到，我增加了一秒钟延迟以触发不正确的结果。把局部变量的值赋给 myglobal 之前，实际上没有什么真正理由要求 thread_function() 睡眠一秒钟。）使用互斥对象的新程序产生了期望的结果： <BR><BR><BR>$ ./thread3 <BR>o..o..o.o..o..o.o.o.o.o..o..o..o.ooooooo <BR>myglobal equals 40 <BR><BR><BR><BR><BR>为了进一步探索这个极为重要的概念，让我们看一看程序中进行加一操作的代码： <BR><BR><BR>thread_function() 加一代码： <BR>j=myglobal; <BR>j=j+1; <BR>printf("."); <BR>fflush(stdout); <BR>sleep(1); <BR>myglobal=j; <BR><BR>主线程加一代码： <BR>myglobal=myglobal+1; <BR><BR><BR><BR><BR>如果代码是位于单线程程序中，可以预期 thread_function() 代码将完整执行。接下来才会执行主线程代码（或者是以相反的顺序执行）。在不使用互斥对象的线程程序中，代码可能（几乎是，由于调用了 sleep() 的缘故）以如下的顺序执行： <BR><BR><BR>thread_function() 线程 主线程 <BR><BR>j=myglobal; <BR>j=j+1; <BR>printf("."); <BR>fflush(stdout); <BR>sleep(1); myglobal=myglobal+1; <BR>myglobal=j; <BR><BR><BR><BR><BR>当代码以此特定顺序执行时，将覆盖主线程对 myglobal 的修改。程序结束后，就将得到不正确的值。如果是在操纵指针的话，就可能产生段错误。注意到 thread_function() 线程按顺序执行了它的所有指令。看来不象是 thread_function() 有什么次序颠倒。问题是，同一时间内，另一个线程对同一数据结构进行了另一个修改。 <BR><BR>线程内幕 1 <BR>在解释如何确定在何处使用互斥对象之前，先来深入了解一下线程的内部工作机制。请看第一个例子： <BR><BR>假设主线程将创建三个新线程：线程 a、线程 b 和线程 c。假定首先创建线程 a，然后是线程 b，最后创建线程 c。 <BR><BR><BR>pthread_create( &amp;thread_a, NULL, thread_function, NULL); <BR>pthread_create( &amp;thread_b, NULL, thread_function, NULL); <BR>pthread_create( &amp;thread_c, NULL, thread_function, NULL); <BR><BR><BR><BR><BR>在第一个 pthread_create() 调用完成后，可以假定线程 a 不是已存在就是已结束并停止。第二个 pthread_create() 调用后，主线程和线程 b 都可以假定线程 a 存在（或已停止）。 <BR><BR>然而，就在第二个 create() 调用返回后，主线程无法假定是哪一个线程（a 或 b）会首先开始运行。虽然两个线程都已存在，线程 CPU 时间片的分配取决于内核和线程库。至于谁将首先运行，并没有严格的规则。尽管线程 a 更有可能在线程 b 之前开始执行，但这并无保证。对于多处理器系统，情况更是如此。如果编写的代码假定在线程 b 开始执行之前实际上执行线程 a 的代码，那么，程序最终正确运行的概率是 99%。或者更糟糕，程序在您的机器上 100% 地正确运行，而在您客户的四处理器服务器上正确运行的概率却是零。 <BR><BR>从这个例子还可以得知，线程库保留了每个单独线程的代码执行顺序。换句话说，实际上那三个 pthread_create() 调用将按它们出现的顺序执行。从主线程上来看，所有代码都是依次执行的。有时，可以利用这一点来优化部分线程程序。例如，在上例中，线程 c 就可以假定线程 a 和线程 b 不是正在运行就是已经终止。它不必担心存在还没有创建线程 a 和线程 b 的可能性。可以使用这一逻辑来优化线程程序。 <BR><BR>线程内幕 2 <BR>现在来看另一个假想的例子。假设有许多线程，他们都正在执行下列代码： <BR><BR><BR>myglobal=myglobal+1; <BR><BR><BR><BR><BR>那么，是否需要在加一操作语句前后分别锁定和解锁互斥对象呢？也许有人会说“不”。编译器极有可能把上述赋值语句编译成一条机器指令。大家都知道，不可能"半途"中断一条机器指令。即使是硬件中断也不会破坏机器指令的完整性。基于以上考虑，很可能倾向于完全省略 pthread_mutex_lock() 和 pthread_mutex_unlock() 调用。不要这样做。 <BR><BR>我在说废话吗？不完全是这样。首先，不应该假定上述赋值语句一定会被编译成一条机器指令，除非亲自验证了机器代码。即使插入某些内嵌汇编语句以确保加一操作的完整执行——甚至，即使是自己动手写编译器！-- 仍然可能有问题。 <BR><BR>答案在这里。使用单条内嵌汇编操作码在单处理器系统上可能不会有什么问题。每个加一操作都将完整地进行，并且多半会得到期望的结果。但是多处理器系统则截然不同。在多 CPU 机器上，两个单独的处理器可能会在几乎同一时刻（或者，就在同一时刻）执行上述赋值语句。不要忘了，这时对内存的修改需要先从 L1 写入 L2 高速缓存、然后才写入主存。（SMP 机器并不只是增加了处理器而已；它还有用来仲裁对 RAM 存取的特殊硬件。）最终，根本无法搞清在写入主存的竞争中，哪个 CPU 将会"胜出"。要产生可预测的代码，应使用互斥对象。互斥对象将插入一道"内存关卡"，由它来确保对主存的写入按照线程锁定互斥对象的顺序进行。 <BR><BR>考虑一种以 32 位块为单位更新主存的 SMP 体系结构。如果未使用互斥对象就对一个 64 位整数进行加一操作，整数的最高 4 位字节可能来自一个 CPU，而其它 4 个字节却来自另一 CPU。糟糕吧！最糟糕的是，使用差劲的技术，您的程序在重要客户的系统上有可能不是很长时间才崩溃一次，就是早上三点钟就崩溃。David R. Butenhof 在他的《POSIX 线程编程》（请参阅本文末尾的参考资料部分）一书中，讨论了由于未使用互斥对象而将产生的种种情况。 <BR><BR>许多互斥对象 <BR>如果放置了过多的互斥对象，代码就没有什么并发性可言，运行起来也比单线程解决方案慢。如果放置了过少的互斥对象，代码将出现奇怪和令人尴尬的错误。幸运的是，有一个中间立场。首先，互斥对象是用于串行化存取*共享数据*。不要对非共享数据使用互斥对象，并且，如果程序逻辑确保任何时候都只有一个线程能存取特定数据结构，那么也不要使用互斥对象。 <BR><BR>其次，如果要使用共享数据，那么在读、写共享数据时都应使用互斥对象。用 pthread_mutex_lock() 和 pthread_mutex_unlock() 把读写部分保护起来，或者在程序中不固定的地方随机使用它们。学会从一个线程的角度来审视代码，并确保程序中每一个线程对内存的观点都是一致和合适的。为了熟悉互斥对象的用法，最初可能要花好几个小时来编写代码，但是很快就会习惯并且*也*不必多想就能够正确使用它们。 <BR><BR>使用调用：初始化 <BR>现在该来看看使用互斥对象的各种不同方法了。让我们从初始化开始。在 thread3.c 示例中，我们使用了静态初始化方法。这需要声明一个 pthread_mutex_t 变量，并赋给它常数 PTHREAD_MUTEX_INITIALIZER： <BR><BR><BR>pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER; <BR><BR><BR><BR><BR>很简单吧。但是还可以动态地创建互斥对象。当代码使用 malloc() 分配一个新的互斥对象时，使用这种动态方法。此时，静态初始化方法是行不通的，并且应当使用例程 pthread_mutex_init()： <BR><BR><BR>int pthread_mutex_init( pthread_mutex_t *mymutex, const pthread_mutexattr_t *attr) <BR><BR><BR><BR>正如所示，pthread_mutex_init 接受一个指针作为参数以初始化为互斥对象，该指针指向一块已分配好的内存区。第二个参数，可以接受一个可选的 pthread_mutexattr_t 指针。这个结构可用来设置各种互斥对象属性。但是通常并不需要这些属性，所以正常做法是指定 NULL。 <BR><BR>一旦使用 pthread_mutex_init() 初始化了互斥对象，就应使用 pthread_mutex_destroy() 消除它。pthread_mutex_destroy() 接受一个指向 pthread_mutext_t 的指针作为参数，并释放创建互斥对象时分配给它的任何资源。请注意， pthread_mutex_destroy() 不会释放用来存储 pthread_mutex_t 的内存。释放自己的内存完全取决于您。还必须注意一点，pthread_mutex_init() 和 pthread_mutex_destroy() 成功时都返回零。 <BR><BR>使用调用：锁定 <BR><BR>pthread_mutex_lock(pthread_mutex_t *mutex) <BR><BR><BR><BR>pthread_mutex_lock() 接受一个指向互斥对象的指针作为参数以将其锁定。如果碰巧已经锁定了互斥对象，调用者将进入睡眠状态。函数返回时，将唤醒调用者（显然）并且调用者还将保留该锁。函数调用成功时返回零，失败时返回非零的错误代码。 <BR><BR><BR>pthread_mutex_unlock(pthread_mutex_t *mutex) <BR><BR><BR><BR><BR>pthread_mutex_unlock() 与 pthread_mutex_lock() 相配合，它把线程已经加锁的互斥对象解锁。始终应该尽快对已加锁的互斥对象进行解锁（以提高性能）。并且绝对不要对您未保持锁的互斥对象进行解锁操作（否则，pthread_mutex_unlock() 调用将失败并带一个非零的 EPERM 返回值）。 <BR><BR><BR>pthread_mutex_trylock(pthread_mutex_t *mutex) <BR><BR><BR><BR><BR>当线程正在做其它事情的时候（由于互斥对象当前是锁定的），如果希望锁定互斥对象，这个调用就相当方便。调用 pthread_mutex_trylock() 时将尝试锁定互斥对象。如果互斥对象当前处于解锁状态，那么您将获得该锁并且函数将返回零。然而，如果互斥对象已锁定，这个调用也不会阻塞。当然，它会返回非零的 EBUSY 错误值。然后可以继续做其它事情，稍后再尝试锁定。 <BR><BR>等待条件发生 <BR>互斥对象是线程程序必需的工具，但它们并非万能的。例如，如果线程正在等待共享数据内某个条件出现，那会发生什么呢？代码可以反复对互斥对象锁定和解锁，以检查值的任何变化。同时，还要快速将互斥对象解锁，以便其它线程能够进行任何必需的更改。这是一种非常可怕的方法，因为线程需要在合理的时间范围内频繁地循环检测变化。 <BR><BR>在每次检查之间，可以让调用线程短暂地进入睡眠，比如睡眠三秒钟，但是因此线程代码就无法最快作出响应。真正需要的是这样一种方法，当线程在等待满足某些条件时使线程进入睡眠状态。一旦条件满足，还需要一种方法以唤醒因等待满足特定条件而睡眠的线程。如果能够做到这一点，线程代码将是非常高效的，并且不会占用宝贵的互斥对象锁。这正是 POSIX 条件变量能做的事！ <BR><BR>而 POSIX 条件变量将是我下一篇文章的主题，其中将说明如何正确使用条件变量。到那时，您将拥有了创建复杂线程程序所需的全部资源，那些线程程序可以模拟工作人员、装配线等等。既然您已经越来越熟悉线程，我将在下一篇文章中加快进度。这样，在下一篇文章的结尾就能放上一个相对复杂的线程程序。说到等到条件产生，下次再见！ <BR>第 3 部分 <BR><BR>内容： <BR><BR>条件变量 <BR>pthread_cond_wait() 小测验 <BR>初始化和清除 <BR>等待 <BR>发送信号和广播 <BR>工作组 <BR>队列 <BR>data_control 代码 <BR>调试时间 <BR>工作组代码 <BR>代码初排 <BR>有关清除的注意事项 <BR>创建工作 <BR>threadfunc() <BR>join_threads() <BR>结束语 <BR>参考资料 <BR>关于作者 <BR><BR><BR>相关内容： <BR><BR>第 2 部分：称作互斥对象的小玩意 <BR>第 1 部分：POSIX 线程详解 <BR><BR>使用条件变量提高效率 <BR>作者:Daniel Robbins <BR><BR><BR>本文是 POSIX 线程三部曲系列的最后一部分，Daniel 将详细讨论如何使用条件变量。条件变量是 POSIX 线程结构，可以让您在遇到某些条件时“唤醒”线程。可以将它们看作是一种线程安全的信号发送。Daniel 使用目前您所学到的知识实现了一个多线程工作组应用程序，本文将围绕着这一示例而进行讨论。 <BR><BR>条件变量详解 <BR>在上一篇文章结束时，我描述了一个比较特殊的难题：如果线程正在等待某个特定条件发生，它应该如何处理这种情况？它可以重复对互斥对象锁定和解锁，每次都会检查共享数据结构，以查找某个值。但这是在浪费时间和资源，而且这种繁忙查询的效率非常低。解决这个问题的最佳方法是使用 pthread_cond_wait() 调用来等待特殊条件发生。 <BR><BR>了解 pthread_cond_wait() 的作用非常重要 -- 它是 POSIX 线程信号发送系统的核心，也是最难以理解的部分。 <BR><BR>首先，让我们考虑以下情况：线程为查看已链接列表而锁定了互斥对象，然而该列表恰巧是空的。这一特定线程什么也干不了 -- 其设计意图是从列表中除去节点，但是现在却没有节点。因此，它只能： <BR><BR>锁定互斥对象时，线程将调用 pthread_cond_wait(&amp;mycond,&amp;mymutex)。pthread_cond_wait() 调用相当复杂，因此我们每次只执行它的一个操作。 <BR><BR>pthread_cond_wait() 所做的第一件事就是同时对互斥对象解锁（于是其它线程可以修改已链接列表），并等待条件 mycond 发生（这样当 pthread_cond_wait() 接收到另一个线程的“信号”时，它将苏醒）。现在互斥对象已被解锁，其它线程可以访问和修改已链接列表，可能还会添加项。 <BR><BR>此时，pthread_cond_wait() 调用还未返回。对互斥对象解锁会立即发生，但等待条件 mycond 通常是一个阻塞操作，这意味着线程将睡眠，在它苏醒之前不会消耗 CPU 周期。这正是我们期待发生的情况。线程将一直睡眠，直到特定条件发生，在这期间不会发生任何浪费 CPU 时间的繁忙查询。从线程的角度来看，它只是在等待 pthread_cond_wait() 调用返回。 <BR><BR>现在继续说明，假设另一个线程（称作“2 号线程”）锁定了 mymutex 并对已链接列表添加了一项。在对互斥对象解锁之后，2 号线程会立即调用函数 pthread_cond_broadcast(&amp;mycond)。此操作之后，2 号线程将使所有等待 mycond 条件变量的线程立即苏醒。这意味着第一个线程（仍处于 pthread_cond_wait() 调用中）现在将苏醒。 <BR><BR>现在，看一下第一个线程发生了什么。您可能会认为在 2 号线程调用 pthread_cond_broadcast(&amp;mymutex) 之后，1 号线程的 pthread_cond_wait() 会立即返回。不是那样！实际上，pthread_cond_wait() 将执行最后一个操作：重新锁定 mymutex。一旦 pthread_cond_wait() 锁定了互斥对象，那么它将返回并允许 1 号线程继续执行。那时，它可以马上检查列表，查看它所感兴趣的更改。 <BR><BR>停止并回顾！ <BR>那个过程非常复杂，因此让我们先来回顾一下。第一个线程首先调用： <BR><BR><BR><BR>pthread_mutex_lock(&amp;mymutex); <BR><BR><BR><BR><BR>然后，它检查了列表。没有找到感兴趣的东西，于是它调用： <BR><BR><BR><BR>pthread_cond_wait(&amp;mycond, &amp;mymutex); <BR><BR><BR><BR><BR>然后，pthread_cond_wait() 调用在返回前执行许多操作： <BR><BR><BR><BR>pthread_mutex_unlock(&amp;mymutex); <BR><BR><BR><BR><BR>它对 mymutex 解锁，然后进入睡眠状态，等待 mycond 以接收 POSIX 线程“信号”。一旦接收到“信号”（加引号是因为我们并不是在讨论传统的 UNIX 信号，而是来自 pthread_cond_signal() 或 pthread_cond_broadcast() 调用的信号），它就会苏醒。但 pthread_cond_wait() 没有立即返回 -- 它还要做一件事：重新锁定 mutex： <BR><BR><BR><BR>pthread_mutex_lock(&amp;mymutex); <BR><BR><BR><BR><BR>pthread_cond_wait() 知道我们在查找 mymutex “背后”的变化，因此它继续操作，为我们锁定互斥对象，然后才返回。 <BR><BR>pthread_cond_wait() 小测验 <BR>现在已回顾了 pthread_cond_wait() 调用，您应该了解了它的工作方式。应该能够叙述 pthread_cond_wait() 依次执行的所有操作。尝试一下。如果理解了 pthread_cond_wait()，其余部分就相当容易，因此请重新阅读以上部分，直到记住为止。好，读完之后，能否告诉我在调用 pthread_cond_wait() 之前，互斥对象必须处于什么状态？pthread_cond_wait() 调用返回之后，互斥对象处于什么状态？这两个问题的答案都是“锁定”。既然已经完全理解了 pthread_cond_wait() 调用，现在来继续研究更简单的东西 -- 初始化和真正的发送信号和广播进程。到那时，我们将会对包含了多线程工作队列的 C 代码了如指掌。 <BR><BR>初始化和清除 <BR>条件变量是一个需要初始化的真实数据结构。以下就初始化的方法。首先，定义或分配一个条件变量，如下所示： <BR><BR><BR><BR><BR>pthread_cond_t mycond; <BR><BR><BR><BR><BR>然后，调用以下函数进行初始化： <BR><BR><BR><BR><BR>pthread_cond_init(&amp;mycond,NULL); <BR><BR><BR><BR><BR>瞧，初始化完成了！在释放或废弃条件变量之前，需要毁坏它，如下所示： <BR><BR><BR><BR><BR>pthread_cond_destroy(&amp;mycond); <BR><BR><BR><BR><BR>很简单吧。接着讨论 pthread_cond_wait() 调用。 <BR><BR>等待 <BR>一旦初始化了互斥对象和条件变量，就可以等待某个条件，如下所示： <BR><BR><BR><BR><BR>pthread_cond_wait(&amp;mycond, &amp;mymutex); <BR><BR><BR><BR><BR>请注意，代码在逻辑上应该包含 mycond 和 mymutex。一个特定条件只能有一个互斥对象，而且条件变量应该表示互斥数据“内部”的一种特殊的条件更改。一个互斥对象可以用许多条件变量（例如，cond_empty、cond_full、cond_cleanup），但每个条件变量只能有一个互斥对象。 <BR><BR>发送信号和广播 <BR>对于发送信号和广播，需要注意一点。如果线程更改某些共享数据，而且它想要唤醒所有正在等待的线程，则应使用 pthread_cond_broadcast 调用，如下所示： <BR><BR><BR><BR><BR>pthread_cond_broadcast(&amp;mycond); <BR><BR><BR><BR><BR>在某些情况下，活动线程只需要唤醒第一个正在睡眠的线程。假设您只对队列添加了一个工作作业。那么只需要唤醒一个工作程序线程（再唤醒其它线程是不礼貌的！）： <BR><BR><BR><BR><BR>pthread_cond_signal(&amp;mycond); <BR><BR><BR><BR><BR>此函数只唤醒一个线程。如果 POSIX 线程标准允许指定一个整数，可以让您唤醒一定数量的正在睡眠的线程，那就更完美了。但是很可惜，我没有被邀请参加会议。 <BR><BR>工作组 <BR>我将演示如何创建多线程工作组。在这个方案中，我们创建了许多工作程序线程。每个线程都会检查 wq（“工作队列”），查看是否有需要完成的工作。如果有需要完成的工作，那么线程将从队列中除去一个节点，执行这些特定工作，然后等待新的工作到达。 <BR><BR>与此同时，主线程负责创建这些工作程序线程、将工作添加到队列，然后在它退出时收集所有工作程序线程。您将会遇到许多 C 代码，好好准备吧！ <BR><BR>队列 <BR>需要队列是出于两个原因。首先，需要队列来保存工作作业。还需要可用于跟踪已终止线程的数据结构。还记得前几篇文章（请参阅本文结尾处的参考资料）中，我曾提到过需要使用带有特定进程标识的 pthread_join 吗？使用“清除队列”（称作 "cq"）可以解决无法等待任何已终止线程的问题（稍后将详细讨论这个问题）。以下是标准队列代码。将此代码保存到文件 queue.h 和 queue.c： <BR><BR>queue.h <BR>/* queue.h <BR>** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc. <BR>** Author: Daniel Robbins <BR>** Date: 16 Jun 2000 <BR>*/ <BR><BR>typedef struct node { <BR>struct node *next; <BR>} node; <BR><BR>typedef struct queue { <BR>node *head, *tail; <BR>} queue; <BR><BR>void queue_init(queue *myroot); <BR>void queue_put(queue *myroot, node *mynode); <BR>node *queue_get(queue *myroot); <BR><BR><BR><BR><BR>queue.c <BR>/* queue.c <BR>** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc. <BR>** Author: Daniel Robbins <BR>** Date: 16 Jun 2000 <BR>** <BR>** This set of queue functions was originally thread-aware. I <BR>** redesigned the code to make this set of queue routines <BR>** thread-ignorant (just a generic, boring yet very fast set of queue <BR>** routines). Why the change? Because it makes more sense to have <BR>** the thread support as an optional add-on. Consider a situation <BR>** where you want to add 5 nodes to the queue. With the <BR>** thread-enabled version, each call to queue_put() would <BR>** automatically lock and unlock the queue mutex 5 times -- thats a <BR>** lot of unnecessary overhead. However, by moving the thread stuff <BR>** out of the queue routines, the caller can lock the mutex once at <BR>** the beginning, then insert 5 items, and then unlock at the end. <BR>** Moving the lock/unlock code out of the queue functions allows for <BR>** optimizations that aren possible otherwise. It also makes this <BR>** code useful for non-threaded applications. <BR>** <BR>** We can easily thread-enable this data structure by using the <BR>** data_control type defined in control.c and control.h. */ <BR><BR>#include &lt;stdio.h&gt; <BR>#include "queue.h" <BR><BR>void queue_init(queue *myroot) { <BR>myroot-&gt;head=NULL; <BR>myroot-&gt;tail=NULL; <BR>} <BR><BR>void queue_put(queue *myroot,node *mynode) { <BR>mynode-&gt;next=NULL; <BR>if (myroot-&gt;tail!=NULL) <BR>myroot-&gt;tail-&gt;next=mynode; <BR>myroot-&gt;tail=mynode; <BR>if (myroot-&gt;:head==NULL) <BR>myroot-&gt;head=mynode; <BR>} <BR><BR>node *queue_get(queue *myroot) { <BR>//get from root <BR>node *mynode; <BR>mynode=myroot-&gt;head; <BR>if (myroot-&gt;head!=NULL) <BR>myroot-&gt;head=myroot-&gt;head-&gt;next; <BR>return mynode; <BR>} <BR><BR><BR><BR><BR>data_control 代码 <BR>我编写的并不是线程安全的队列例程，事实上我创建了一个“数据包装”或“控制”结构，它可以是任何线程支持的数据结构。看一下 control.h： <BR><BR><BR>control.h <BR>#include <BR><BR>typedef struct data_control { <BR>pthread_mutex_t mutex; <BR>pthread_cond_t cond; <BR>int active; <BR>} data_control; <BR><BR><BR><BR><BR>现在您看到了 data_control 结构定义，以下是它的视觉表示： <BR><BR>所使用的 data_control 结构 <BR><BR><BR><BR>图像中的锁代表互斥对象，它允许对数据结构进行互斥访问。黄色的星代表条件变量，它可以睡眠，直到所讨论的数据结构改变为止。on/off 开关表示整数 "active"，它告诉线程此数据是否是活动的。在代码中，我使用整数 active 作为标志，告诉工作队列何时应该关闭。以下是 control.c： <BR><BR>control.c <BR>/* control.c <BR>** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc. <BR>** Author: Daniel Robbins <BR>** Date: 16 Jun 2000 <BR>** <BR>** These routines provide an easy way to make any type of <BR>** data-structure thread-aware. Simply associate a data_control <BR>** structure with the data structure (by creating a new struct, for <BR>** example). Then, simply lock and unlock the mutex, or <BR>** wait/signal/broadcast on the condition variable in the data_control <BR>** structure as needed. <BR>** <BR>** data_control structs contain an int called "active". This int is <BR>** intended to be used for a specific kind of multithreaded design, <BR>** where each thread checks the state of "active" every time it locks <BR>** the mutex. If active is 0, the thread knows that instead of doing <BR>** its normal routine, it should stop itself. If active is 1, it <BR>** should continue as normal. So, by setting active to 0, a <BR>** controlling thread can easily inform a thread work crew to shut <BR>** down instead of processing new jobs. Use the control_activate() <BR>** and control_deactivate() functions, which will also broadcast on <BR>** the data_control structs condition variable, so that all threads <BR>** stuck in pthread_cond_wait() will wake up, have an opportunity to <BR>** notice the change, and then terminate. <BR>*/ <BR><BR>#include "control.h" <BR><BR>int control_init(data_control *mycontrol) { <BR>int mystatus; <BR>if (pthread_mutex_init(&amp;(mycontrol-&gt;mutex),NULL)) <BR>return 1; <BR>if (pthread_cond_init(&amp;(mycontrol-&gt;cond),NULL)) <BR>return 1; <BR>mycontrol-&gt;active=0; <BR>return 0; <BR>} <BR><BR>int control_destroy(data_control *mycontrol) { <BR>int mystatus; <BR>if (pthread_cond_destroy(&amp;(mycontrol-&gt;cond))) <BR>return 1; <BR>if (pthread_cond_destroy(&amp;(mycontrol-&gt;cond))) <BR>return 1; <BR>mycontrol-&gt;active=0; <BR>return 0; <BR>} <BR>int control_activate(data_control *mycontrol) { <BR>int mystatus; <BR>if (pthread_mutex_lock(&amp;(mycontrol-&gt;mutex))) <BR>return 0; <BR>mycontrol-&gt;active=1; <BR>pthread_mutex_unlock(&amp;(mycontrol-&gt;mutex)); <BR>pthread_cond_broadcast(&amp;(mycontrol-&gt;cond)); <BR>return 1; <BR>} <BR><BR>int control_deactivate(data_control *mycontrol) { <BR>int mystatus; <BR>if (pthread_mutex_lock(&amp;(mycontrol-&gt;mutex))) <BR>return 0; <BR>mycontrol-&gt;active=0; <BR>pthread_mutex_unlock(&amp;(mycontrol-&gt;mutex)); <BR>pthread_cond_broadcast(&amp;(mycontrol-&gt;cond)); <BR>return 1; <BR>} <BR><BR><BR><BR><BR><BR>调试时间 <BR>在开始调试之前，还需要一个文件。以下是 dbug.h： <BR><BR>dbug.h <BR>#define dabort() <BR>{ printf("Aborting at line %d in source file %sn",__LINE__,__FILE__); abort(); } <BR><BR><BR><BR><BR>此代码用于处理工作组代码中的不可纠正错误。 <BR><BR>工作组代码 <BR>说到工作组代码，以下就是： <BR><BR><BR>workcrew.c <BR>#include &lt;stdio.h&gt; <BR>#include &lt;stdlib.h&gt; <BR>#include "control.h" <BR>#include "queue.h" <BR>#include "dbug.h" <BR><BR>/* the work_queue holds tasks for the various threads to complete. */ <BR><BR>struct work_queue { <BR>data_control control; <BR>queue work; <BR>} wq; <BR><BR><BR>/* I added a job number to the work node. Normally, the work node <BR>would contain additional data that needed to be processed. */ <BR><BR>typedef struct work_node { <BR>struct node *next; <BR>int jobnum; <BR>} wnode; <BR><BR>/* the cleanup queue holds stopped threads. Before a thread <BR>terminates, it adds itself to this list. Since the main thread is <BR>waiting for changes in this list, it will then wake up and clean up <BR>the newly terminated thread. */ <BR><BR>struct cleanup_queue { <BR>data_control control; <BR>queue cleanup; <BR>} cq; <BR><BR>/* I added a thread number (for debugging/instructional purposes) and <BR>a thread id to the cleanup node. The cleanup node gets passed to <BR>the new thread on startup, and just before the thread stops, it <BR>attaches the cleanup node to the cleanup queue. The main thread <BR>monitors the cleanup queue and is the one that performs the <BR>necessary cleanup. */ <BR><BR>typedef struct cleanup_node { <BR>struct node *next; <BR>int threadnum; <BR>pthread_t tid; <BR>} cnode; <BR><BR>void *threadfunc(void *myarg) { <BR><BR>wnode *mywork; <BR>cnode *mynode; <BR><BR>mynode=(cnode *) myarg; <BR><BR>pthread_mutex_lock(&amp;wq.control.mutex); <BR><BR>while (wq.control.active) { <BR>while (wq.work.head==NULL &amp;&amp; wq.control.active) { <BR>pthread_cond_wait(&amp;wq.control.cond, &amp;wq.control.mutex); <BR>} <BR>if (!wq.control.active) <BR>break; <BR>//we got something! <BR>mywork=(wnode *) queue_get(&amp;wq.work); <BR>pthread_mutex_unlock(&amp;wq.control.mutex); <BR>//perform processing... <BR>printf("Thread number %d processing job %dn",mynode-&gt;threadnum,mywork-&gt;jobnum); <BR>free(mywork); <BR>pthread_mutex_lock(&amp;wq.control.mutex); <BR>} <BR><BR>pthread_mutex_unlock(&amp;wq.control.mutex); <BR><BR>pthread_mutex_lock(&amp;cq.control.mutex); <BR>queue_put(&amp;cq.cleanup,(node *) mynode); <BR>pthread_mutex_unlock(&amp;cq.control.mutex); <BR>pthread_cond_signal(&amp;cq.control.cond); <BR>printf("thread %d shutting down...n",mynode-&gt;threadnum); <BR>return NULL; <BR><BR>} <BR><BR>#define NUM_WORKERS 4 <BR><BR>int numthreads; <BR><BR>void join_threads(void) { <BR>cnode *curnode; <BR><BR>printf("joining threads...n"); <BR><BR>while (numthreads) { <BR>pthread_mutex_lock(&amp;cq.control.mutex); <BR><BR>/* below, we sleep until there really is a new cleanup node. This <BR>takes care of any false wakeups... even if we break out of <BR>pthread_cond_wait(), we don make any assumptions that the <BR>condition we were waiting for is true. */ <BR><BR>while (cq.cleanup.head==NULL) { <BR>pthread_cond_wait(&amp;cq.control.cond,&amp;cq.control.mutex); <BR>} <BR><BR>/* at this point, we hold the mutex and there is an item in the <BR>list that we need to process. First, we remove the node from <BR>the queue. Then, we call pthread_join() on the tid stored in <BR>the node. When pthread_join() returns, we have cleaned up <BR>after a thread. Only then do we free() the node, decrement the <BR>number of additional threads we need to wait for and repeat the <BR>entire process, if necessary */ <BR><BR>curnode = (cnode *) queue_get(&amp;cq.cleanup); <BR>pthread_mutex_unlock(&amp;cq.control.mutex); <BR>pthread_join(curnode-&gt;tid,NULL); <BR>printf("joined with thread %dn",curnode-&gt;threadnum); <BR>free(curnode); <BR>numthreads--; <BR>} <BR>} <BR><BR><BR>int create_threads(void) { <BR>int x; <BR>cnode *curnode; <BR><BR>for (x=0; x&lt;NUM_WORKERS; x++) { <BR>curnode=malloc(sizeof(cnode)); <BR>if (!curnode) <BR>return 1; <BR>curnode-&gt;threadnum=x; <BR>if (pthread_create(&amp;curnode-&gt;tid, NULL, threadfunc, (void *) curnode)) <BR>return 1; <BR>printf("created thread %dn",x); <BR>numthreads++; <BR>} <BR>return 0; <BR>} <BR><BR>void initialize_structs(void) { <BR>numthreads=0; <BR>if (control_init(&amp;wq.control)) <BR>dabort(); <BR>queue_init(&amp;wq.work); <BR>if (control_init(&amp;cq.control)) { <BR>control_destroy(&amp;wq.control); <BR>dabort(); <BR>} <BR>queue_init(&amp;wq.work); <BR>control_activate(&amp;wq.control); <BR>} <BR><BR>void cleanup_structs(void) { <BR>control_destroy(&amp;cq.control); <BR>control_destroy(&amp;wq.control); <BR>} <BR><BR><BR>int main(void) { <BR><BR>int x; <BR>wnode *mywork; <BR><BR>initialize_structs(); <BR><BR>/* CREATION */ <BR><BR>if (create_threads()) { <BR>printf("Error starting threads... cleaning up.n"); <BR>join_threads(); <BR>dabort(); <BR>} <BR><BR>pthread_mutex_lock(&amp;wq.control.mutex); <BR>for (x=0; x&lt;16000; x++) { <BR>mywork=malloc(sizeof(wnode)); <BR>if (!mywork) { <BR>printf("ouch! can malloc!n"); <BR>break; <BR>} <BR>mywork-&gt;jobnum=x; <BR>queue_put(&amp;wq.work,(node *) mywork); <BR>} <BR>pthread_mutex_unlock(&amp;wq.control.mutex); <BR>pthread_cond_broadcast(&amp;wq.control.cond); <BR><BR>printf("sleeping...n"); <BR>sleep(2); <BR>printf("deactivating work queue...n"); <BR>control_deactivate(&amp;wq.control); <BR>/* CLEANUP */ <BR><BR>join_threads(); <BR>cleanup_structs(); <BR><BR>} <BR><BR><BR><BR><BR>代码初排 <BR>现在来快速初排代码。定义的第一个结构称作 "wq"，它包含了 data_control 和队列头。data_control 结构用于仲裁对整个队列的访问，包括队列中的节点。下一步工作是定义实际的工作节点。要使代码符合本文中的示例，此处所包含的都是作业号。 <BR><BR>接着，创建清除队列。注释说明了它的工作方式。好，现在让我们跳过 threadfunc()、join_threads()、create_threads() 和 initialize_structs() 调用，直接跳到 main()。所做的第一件事就是初始化结构 -- 这包括初始化 data_controls 和队列，以及激活工作队列。 <BR><BR>有关清除的注意事项 <BR>现在初始化线程。如果看一下 create_threads() 调用，似乎一切正常 -- 除了一件事。请注意，我们正在分配清除节点，以及初始化它的线程号和 TID 组件。我们还将清除节点作为初始自变量传递给每一个新的工作程序线程。为什么这样做？ <BR><BR>因为当某个工作程序线程退出时，它会将其清除节点连接到清除队列，然后终止。那时，主线程会在清除队列中检测到这个节点（利用条件变量），并将这个节点移出队列。因为 TID（线程标识）存储在清除节点中，所以主线程可以确切知道哪个线程已终止了。然后，主线程将调用 pthread_join(tid)，并联接适当的工作程序线程。如果没有做记录，那么主线程就需要按任意顺序联接工作程序线程，可能是按它们的创建顺序。由于线程不一定按此顺序终止，那么主线程可能会在已经联接了十个线程时，等待联接另一个线程。您能理解这种设计决策是如何使关闭代码加速的吗（尤其在使用几百个工作程序线程的情况下）？ <BR><BR>创建工作 <BR>我们已启动了工作程序线程（它们已经完成了执行 threadfunc()，稍后将讨论此函数），现在主线程开始将工作节点插入工作队列。首先，它锁定 wq 的控制互斥对象，然后分配 16000 个工作包，将它们逐个插入队列。完成之后，将调用 pthread_cond_broadcast()，于是所有正在睡眠的线程会被唤醒，并开始执行工作。此时，主线程将睡眠两秒钟，然后释放工作队列，并通知工作程序线程终止活动。接着，主线程会调用 join_threads() 函数来清除所有工作程序线程。 <BR><BR>threadfunc() <BR>现在来讨论 threadfunc()，这是所有工作程序线程都要执行的代码。当工作程序线程启动时，它会立即锁定工作队列互斥对象，获取一个工作节点（如果有的话），然后对它进行处理。如果没有工作，则调用 pthread_cond_wait()。您会注意到这个调用在一个非常紧凑的 while() 循环中，这是非常重要的。当从 pthread_cond_wait() 调用中苏醒时，决不能认为条件肯定发生了 -- 它可能发生了，也可能没有发生。如果发生了这种情况，即错误地唤醒了线程，而列表是空的，那么 while 循环将再次调用 pthread_cond_wait()。 <BR><BR>如果有一个工作节点，那么我们只打印它的作业号，释放它并退出。然而，实际代码会执行一些更实质性的操作。在 while() 循环结尾，我们锁定了互斥对象，以便检查 active 变量，以及在循环顶部检查新的工作节点。如果执行完此代码，就会发现如果 wq.control.active 是 0，while 循环就会终止，并会执行 threadfunc() 结尾处的清除代码。 <BR><BR>工作程序线程的清除代码部件非常有趣。首先，由于 pthread_cond_wait() 返回了锁定的互斥对象，它会对 work_queue 解锁。然后，它锁定清除队列，添加清除代码（包含了 TID，主线程将使用此 TID 来调用 pthread_join()），然后再对清除队列解锁。此后，它发信号给所有 cq 等待者 (pthread_cond_signal(&amp;cq.control.cond))，于是主线程就知道有一个待处理的新节点。我们不使用 pthread_cond_broadcast()，因为没有这个必要 -- 只有一个线程（主线程）在等待清除队列中的新节点。当它调用 join_threads() 时，工作程序线程将打印关闭消息，然后终止，等待主线程发出的 pthread_join() 调用。 <BR><BR>join_threads() <BR>如果要查看关于如何使用条件变量的简单示例，请参考 join_threads() 函数。如果还有工作程序线程，join_threads() 会一直执行，等待清除队列中新的清除节点。如果有新节点，我们会将此节点移出队列、对清除队列解锁（从而使工作程序可以添加清除节点）、联接新的工作程序线程（使用存储在清除节点中的 TID）、释放清除节点、减少“现有”线程的数量，然后继续。 <BR><BR>结束语 <BR>现在已经到了“POSIX 线程详解”系列的尾声，希望您已经准备好开始将多线程代码添加到您自己的应用程序中。有关详细信息，请参阅参考资料部分，这部分内容还包含了本文中使用的所有源码的 tar 文件。下一个系列中再见！ <BR><BR>参考资料 <BR><BR><BR>本文中使用的源码的 tar 文件。 <BR>友好的 Linux pthread 在线帮助 ("man -k pthread") 是极好的参考资料。 <BR>如果要彻底了解 POSIX 线程，我推荐此书：Programming with POSIX Threads，David R. Butenhof (Addison-Wesley, 1997)。据证实，此书是现有最好的讨论 POSIX 线程的书籍。 <BR>W. Richard Stevens 撰写的 UNIX Network Programming - Networking APIs: Sockets and XTI，(Prentice Hall, 1997) 一书还涵盖了 POSIX 线程。这是一本经典著作，但它讨论线程不如上述的 Programming with POSIX Threads 那样详细。 <BR>请参考 Daniel 在 developerWorks 上发表的 POSIX 线程系列中的前几篇文章： <BR>POSIX 线程详解介绍了 POSIX 线程，并演示了如何在代码中使用线程。 <BR>POSIX 线程详解，第 2 部分演示了如何使用被称为互斥对象的灵巧小玩意，来保护线程代码中共享数据结构的完整性。 <BR>请参阅 Sean Walton 撰写的有关 Linux 线程的文档，KB7rfa <BR>请学习亚里桑那大学的 Mark Hays 编写的 POSIX 线程教程。 <BR>请在 Pthreads-Tcl 介绍中查看对 Tcl 的更改，此更改使 Tcl 能够与 POSIX 线程一起使用。 <BR>请学习另一个教程 POSIX 线程入门, 此教程由位于阿姆赫斯特镇的马萨诸塞大学计算机科学系的 Tom Wagner 和 Don Towsley 编写。 <BR>FSU PThreads 是实现 POSIX 线程 (基于 SunOS 4.1.x, Solaris 2.x, SCO UNIX, FreeBSD, Linux, 和 DOS）的 C 类库。 <BR>请访问 LINUX POSIX 和 DCE 线程主页。 <BR>请参阅 LinuxThreads 资料库。 <BR>Proolix 是一种简单的遵从 POSIX 标准的基于 i8086+ 的操作系统。 <BR>关于作者 <BR>Daniel Robbins 居住在新墨西哥州的 Albuquerque。他是 Gentoo Technologies, Inc. 的总裁兼 CEO，Gentoo 项目的总设计师，MacMillan 出版书籍的撰稿作者，他的著作有：Caldera OpenLinux Unleashed, SuSE Linux Unleashed, 和 Samba Unleashed。Daniel 自二年级起就与计算机某些领域结下不解之缘，那时他首先接触的是 Logo 程序语言，并沉溺于 Pac-Man 游戏中。这也许就是他至今仍担任 SONY Electronic Publishing/Psygnosis 的首席图形设计师的原因所在。Daniel 喜欢与妻子 Mary 和新出生的女儿 Hadassah 一起共度时光。可通过 <A style="COLOR: #003793" href="mailto:drobbins@gentoo.org">drobbins@gentoo.org</A> 与 Daniel 联系。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/490.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:38 <a href="http://www.cnitblog.com/drizztzou/articles/490.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux编程风格 0.1</title><link>http://www.cnitblog.com/drizztzou/articles/489.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:34:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/489.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/489.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/489.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/489.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/489.html</trackback:ping><description><![CDATA[一、GNU风格<BR><BR>1.函数返回类型说明和函数名分两行放置，函数起始字符和函数开头左花括号放到最左边，<BR><BR><BR><BR>例如：<BR><BR>static char *<BR><BR>main (argc, argv)<BR><BR>&nbsp;&nbsp; int argc;<BR><BR>&nbsp;&nbsp; char *argv[];<BR><BR>{<BR><BR> ......<BR><BR>}<BR><BR><BR><BR>或者是用标准C：<BR><BR>static char *<BR><BR>main (int argc, char *argv[])<BR><BR>{<BR><BR> ......<BR><BR>}<BR><BR><BR><BR>如果参数太长不能放到一行，请在每行参数开头处对齐：<BR><BR>int<BR><BR>net_connect (struct sockaddr_in *cs, char *server, unsigned short int port,<BR><BR>	&nbsp;&nbsp;&nbsp;&nbsp; char *sourceip, unsigned short int sourceport, int sec)<BR><BR><BR><BR>对于函数体，我们应该按照如何方式排版：<BR><BR>　　在左括号之前、逗号之后，以及运算符号前后添加空格使程序便于阅读，例如：<BR><BR><BR><BR>if (x &lt; foo (y, z))<BR><BR>&nbsp; haha = bar[4] + 5;<BR><BR>else<BR><BR>&nbsp; {<BR><BR>&nbsp;&nbsp; while (z)<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; haha += foo (z, z);<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; z--;<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<BR><BR>&nbsp;&nbsp;&nbsp; return ++x + bar ();<BR><BR>}<BR><BR><BR><BR>当一个表达式需要分成多行书写的时候，应该在操作符之前分割。例如：<BR><BR>if (foo_this_is_long &amp;&amp; bar &gt; win (x, y, z)<BR><BR>&nbsp;&nbsp;&nbsp; &amp;&amp; remaining_condition)<BR><BR><BR><BR>2. 尽量不要让两个不同优先级的操作符出现在相同的对齐方式中,应该附加额外的括号使得<BR><BR>代码缩进可以表示出嵌套。例如：<BR><BR>错误的对齐：<BR><BR>mode = (inmode[j] == VOIDmode<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; || GET_MODE_SIZE (outmode[j]) &gt; GET_MODE_SIZE (inmode[j])<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ? outmode[j] : inmode[j];<BR><BR>正确的对齐：<BR><BR>mode = ((inmode[j] == VOIDmode<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; || (GET_MODE_SIZE (outmode[j]) &gt; GET_MODE_SIZE (inmode[j])))<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ? outmode[j] : inmode[j];<BR><BR><BR><BR>3. 按照如下方式排版do-while语句：<BR><BR>do<BR><BR>&nbsp; {<BR><BR>&nbsp;&nbsp;&nbsp; a = foo (a);<BR><BR>&nbsp; }<BR><BR>while (a &gt; 0);<BR><BR><BR><BR>4. 每个程序都应该以一段简短的说明其功能的注释开头。例如：<BR><BR>/* fmt - filter for simple filling of text */<BR><BR><BR><BR>5. 请为每个函数书写注释，说明函数做了什么，需要那些种类的参数，参数可能值的含义<BR><BR>和用途。如果用了非常见的、非标准的东西，或者可能导致函数不能工作的任何可能的值，<BR><BR>应该对他们进行说明。如果存在重要的返回值，也需要说明。<BR><BR><BR><BR>6. 不要声明多个变量时跨行，每一行都以一个新的声明开头。例如：<BR><BR>错误的声明：<BR><BR>int foo,<BR><BR>&nbsp;&nbsp;&nbsp; bar;<BR><BR>正确的声明：<BR><BR>int foo, bar;<BR><BR>或者：<BR><BR>int foo;<BR><BR>int bar;<BR><BR>如果他们是全局变量，在每一个之前都应该注释。<BR><BR><BR><BR>7. 当一个if中嵌套了另一个if-else时，应用花括号把if-else括起来。例如，不要写：<BR><BR>if (foo)<BR><BR>&nbsp; if (bar)<BR><BR>&nbsp;&nbsp;&nbsp; win ();<BR><BR>&nbsp; else<BR><BR>&nbsp;&nbsp;&nbsp; lose ();<BR><BR>而要写：<BR><BR>if (foo)<BR><BR>&nbsp; {<BR><BR>&nbsp;&nbsp;&nbsp; if (bar)<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; win ();<BR><BR>&nbsp;&nbsp;&nbsp; else<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lose ();<BR><BR>&nbsp;&nbsp; }<BR><BR>如果再else中嵌套了一个if,可以这样写else if:<BR><BR>if (foo)<BR><BR>&nbsp; ...<BR><BR>else if (bar)<BR><BR>&nbsp; ...<BR><BR>　　按照与then那部分代码相同的缩进方式缩进else if的then部分代码，也可以在花括号<BR><BR>中像下面那样把if嵌套起来：<BR><BR>if (foo)<BR><BR>&nbsp; ...<BR><BR>else<BR><BR>&nbsp; {<BR><BR>&nbsp;&nbsp;&nbsp; if (bar)<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<BR><BR>&nbsp;&nbsp; }<BR><BR>&nbsp;&nbsp; <BR><BR>8. 要在同一个声明中同时说明结构标识和变量或者结构标识和类型定义(typedef)。单独的<BR><BR>说明结构标识，而后用它定义的变量或者定义类型。<BR><BR><BR><BR>9. 尽量避免在if的条件中进行赋值。例如，不要写：<BR><BR>if ((foo = (char *) malloc (sizeof *foo)) == 0)<BR><BR>&nbsp; fatal ("virtual memory exhausted");<BR><BR>而要写：<BR><BR>foo = (char *) malloc (sizeof *foo);<BR><BR>if (foo == 0)<BR><BR>&nbsp; fatal ("virtual memory exhausted");<BR><BR>&nbsp; <BR><BR>10. 请在名子中使用下划线以分割单词，尽量适用小写；把大写字母留给宏和枚举常量，以<BR><BR>及根据统一惯例使用前缀。例如，应该使用类似ignore_space_change_flag的名子；不要使<BR><BR>用类似iCantReadThis的名子。<BR><BR><BR><BR>11. 用于表明一个命令行选项是否给出的变量应该在选项含义的说明之后，而不是选项字符<BR><BR>之后被命名。一条注释既应该说明选项的精确含义，又应该说明选项的字母。例如：<BR><BR>/* ignore changes in horizontal whitespace (-b). */<BR><BR>int ignore_space_change_flag;<BR><BR><BR><BR>二、Linux 内核编程风格<BR><BR><BR><BR>1. Linux内核缩进风格是8个字符。<BR><BR><BR><BR>2. Linux内核风格采用K&amp;R标准，将开始的大括号放在以行的最后，而将结束的大括号放在<BR><BR>以行的第一位，如下：<BR><BR>if (x == 1) {<BR><BR>......<BR><BR>}<BR><BR>命名函数时，开始的括号使放在下一行的第一位，如下：<BR><BR>int function(int x)<BR><BR>{<BR><BR>......<BR><BR>}<BR><BR>　　结束的括号在它所占的那一行是空的，除了它还可以跟随着同一条语句的继续符号。如<BR><BR>while在do-while循环，或者else在if语句中。如下：<BR><BR>do {<BR><BR>......<BR><BR>) while (condition);<BR><BR>以及：<BR><BR>if (x == y) {<BR><BR>...<BR><BR>} else if (x &gt; y) {<BR><BR>...<BR><BR>) else {<BR><BR>...<BR><BR>}<BR><BR><BR><BR>3. 命名尽量简洁。不应该使用诸如 ThisVariableIsATemporaryCounter之类的名子。应该<BR><BR>名名为tmp，这样容易书写，也不难理解。但是命名全局变量，就应该用描述性命名方式，<BR><BR>例如应该命名count_active_users()，而不是cntusr()。本地变量应该避免过长。<BR><BR><BR><BR>4. 函数最好短小精悍，一般来说不要让函数的参数多于１０个，否则你应该尝试分解这个<BR><BR>过于复杂的函数。<BR><BR><BR><BR>5. 通常情况，注释说明代码的功能，而不是其实现原理。避免把注释插到函数体内，而写<BR><BR>到函数前面，说明其功能，如果这个函数的确很复杂，需要其中有部分注释，可以写些简短<BR><BR>的注释来说明或禁告那些重要的部分，但是不能过多。<BR><BR><BR><BR>如果你感觉这些规则过于复杂，有一个小工具可以帮助你——indent。例如你要把你代码转换<BR><BR>成GNU或Linux核心风格，你可以分别使用：<BR><BR>indent -gnu test.c<BR><BR>indent -kr -i8 test.c<BR><BR>其他用法请使用man indent查询。<img src ="http://www.cnitblog.com/drizztzou/aggbug/489.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:34 <a href="http://www.cnitblog.com/drizztzou/articles/489.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>GNU make 指南</title><link>http://www.cnitblog.com/drizztzou/articles/488.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:33:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/488.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/488.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/488.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/488.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/488.html</trackback:ping><description><![CDATA[　译者按： 本文是一篇介绍 GNU Make 的文章，读完后读者应该基本掌握了 make 的用法。而 make 是所有想在 Unix （当然也包括 Linux ）系统上编程的用户必须掌握的工具。如果你写的程序中没有用到 make ，则说明你写的程序只是个人的练习程序，不具有任何实用的价值。也许这么说有点 儿偏激，但 make 实在是应该用在任何稍具规模的程序中的。希望本文可以为中国的 Unix 编程初学者提供一点儿有用的资料。中国的 Linux 用户除了学会安装红帽子以外， 实在应该尝试写一些有用的程序。个人想法，大家参考。 <BR>0) 介绍 <BR>　　~~~~~~~~~~~~~~~ <BR>　　本文将首先介绍为什么要将你的Ｃ源代码分离成几个合理的独立档案，什么时候需要分，怎么才能分的好。然后将会告诉你 GNU Make 怎样使你的编译和连接步骤自动化。对于其它 Make 工具的用户来说，虽然在用其它类似工具时要做适当的调整，本文的内容仍然是非常有用的。如果对你自己的编程工具有怀疑，可以实际的试一试，但请先阅读用户手册。 <BR><BR>　　1) 多文件项目 <BR>　　~~~~~~~~~~~~~~~~~~~~~~ <BR>　　1.1为什么使用它们? <BR>　　首先，多文件项目的好处在那里呢？ <BR>　　它们看起来把事情弄的复杂无比。又要 header 文件，又要 extern 声明，而且如果需要查找一个文件，你要在更多的文件里搜索。 <BR><BR>　　但其实我们有很有力的理由支持我们把一个项目分解成小块。当你改动一行代码，编译器需要全部重新编译来生成一个新的可执行文件。但如果你的项目是分开在几个小文件里，当你改动其中一个文件的时候，别的源文件的目标文件(object files)已经存在，所以没有什么原因去重新编译它们。你所需要做的只是重现编译被改动过的那个文件，然后重新连接所有的目标文件罢了。在大型的项目中，这意味着从很长的（几分钟到几小时）重新编译缩短为十几，二十几秒的简单调整。 <BR><BR>　　只要通过基本的规划，将一个项目分解成多个小文件可使你更加容易的找到一段代码。很简单，你根据代码的作用把你的代码分解到不同的文件里。当你要看一段代码时，你可以准确的知道在那个文件中去寻找它。 <BR><BR>　　从很多目标文件生成一个程序包 (Library)比从一个单一的大目标文件生成要好的多。当然实际上这是否真是一个优势则是由你所用的系统来决定的。但是当使用 gcc/ld (一个 GNU C 编译／连接器) 把一个程序包连接到一个程序时，在连接的过程中，它会尝试不去连接没有使用到的部分。但它每次只能从程序包中把一个完整的目标文件排除在外。因此如果你参考一个程序包中某一个目标档中任何一个符号的话，那么这个目标文件整个都会被连接进来。要是一个程序包被非常充分的分解了的话，那么经连接后，得到的可执行文件会比从一个大目标文件组成的程序包连接得到的文件小得多。 <BR><BR>　　又因为你的程序是很模块化的，文件之间的共享部分被减到最少，那就有很多好处——可以很容易的追踪到臭虫，这些模块经常是可以用在其它的项目里的，同时别人也可以更容易的理解你的一段代码是干 什么的。当然此外还有许多别的好处…… <BR><BR>　　1.2 何时分解你的项目 <BR><BR>　　很明显，把任何东西都分解是不合理的。象“世界，你们好”这样的简单程序根本就不能分，因为实在也没什么可分的。把用于测试用的小程序分解也是没什么意思的。但一般来说，当分解项目有助于布局、发展和易读性的时候，我都会采取它。在大多数的情况下，这都是适用的。（所谓“世界，你们好”，既 hello world ，只是一个介绍一种编程语言时惯用的范例程序，它会在屏幕上显示一行 hello world 。是最简单的程序。） <BR><BR>　　如果你需要开发一个相当大的项目，在开始前，应该考虑一下你将如何实现它，并且生成几个文件（用适当的名字）来放你的代码。当然，在你的项目开发的过程中，你可以建立新的文件，但如果你这么做的话，说明你可能改变了当初的想法，你应该想想是否需要对整体结构也进行相应的调整。 <BR><BR>　　对于中型的项目，你当然也可以采用上述技巧，但你也可以就那么开始输入你的代码，当你的码多到难以管理的时候再把它们分解成不同的档案。但以我的经验来说，开始时在脑子里形成一个大概的方案，并且尽量遵从它，或在开发过程中，随着程序的需要而修改，会使开发变得更加容易。 <BR><BR>　　1.3 怎样分解项目 <BR><BR>　　先说明，这完全是我个人的意见，你可以（也许你真的会？）用别的方式来做。这会触动到有关编码风格的问题，而大家从来就没有停止过在这个问题上的争论。在这里我只是给出我自己喜欢的做法（同时也给出这么做的原因）： <BR><BR>　　i) 不要用一个 header 文件指向多个源码文件（例外：程序包 的 header 文件）。用一个 header定义一个源码文件的方式 会更有效，也更容易查寻。否则改变一个源文件的结构（并且 它的 header 文件）就必须重新编译好几个文件。 <BR><BR>　　ii) 如果可以的话，完全可以用超过一个的 header 文件来指向同 一个源码文件。有时将不可公开调用的函数原型，类型定义 等等，从它们的Ｃ源码文件中分离出来是非常有用的。使用一 个 header 文件装公开符号，用另一个装私人符号意味着如果 你改变了这个源码文件的内部结构，你可以只是重新编译它而 不需要重新编译那些使用它的公开 header 文件的其它的源文 件。 <BR>　　iii) 不要在多个 header 文件中重复定义信息。 如果需要， 在其中一个 header 文件里 #include 另一个，但 是不要重复输入相同的 header 信息两次。原因是如果你以后改 变了这个信息，你只需要把它改变一次，不用搜索并改变另外一 个重复的信息。 <BR>　　iv) 在每一个源码文件里， #include 那些声明了源码文件中的符 号的所有 header 文件。这样一来，你在源码文件和 header 文件对某些函数做出的矛盾声明可以比较容易的被编译器发现。 <BR><BR>　　1.4 对于常见错误的注释 <BR>　　a) 定义符 (Identifier) 在源码文件中的矛盾：在Ｃ里，变量和函数的缺 省状态是公用的。因此，任何Ｃ源码档案都可以引用存在于其它源 码档中的通用 (global) 函数和通用变量，既使这个档案没有那个变 量或函数的声明或原型。因此你必须保证在不同的两个档案里不能 用同一个符号名称，否则会有连接错误或者在编译时会有警告。 <BR>　　一种避免这种错误的方法是在公用的符号前加上跟其所在源文件有 关的前缀。比如：所有在 gfx.c 里的函数都加上前缀“gfx_”。如果 你很小心的分解你的程序，使用有意义的函数名称，并且不是过分 使用通用变量，当然这根本就不是问题。 <BR>　　要防止一个符号在它被定义的源文件以外被看到，可在它的定义前 加上关键字“static”。这对只在一个档案内部使用，其它档案都 都不会用到的简单函数是很有用的。 <BR>　　b) 多次定义的符号： header 档会被逐字的替换到你源文件里 #include 的位置的。因此，如果 header 档被 #include 到一个以上的源文件 里，这个 header 档中所有的定义就会出现在每一个有关的源码文件 里。这会使它们里的符号被定义一次以上，从而出现连接错误（见 上）。 <BR><BR>　　解决方法： 不要在 header 档里定义变量。你只需要在 header 档里声明它们然后在适当的Ｃ源码文件（应该 #include 那个 header 档的那个）里定义它们（一次）。对于初学者来说，定义和声明是 很容易混淆的。声明的作用是告诉编译器其所声明的符号应该存在， 并且要有所指定的类型。但是，它并不会使编译器分配贮存空间。 而定义的做用是要求编译器分配贮存空间。当做一个声明而不是做 定义的时候，在声明前放一个关键字“extern”。 <BR><BR>　　例如，我们有一个叫“counter”的变量，如果想让它成为公用的， 我们在一个源码程序（只在一个里面）的开始定义它：“int counter;”，再在相关的 header 档里声明它：“extern int counter;”。 <BR><BR>　　函数原型里隐含着 extern 的意思，所以不需顾虑这个问题。 <BR><BR>　　c) 重复定义，重复声明，矛盾类型： <BR>　　请考虑如果在一个Ｃ源码文件中 #include 两个档 a.h 和 b.h， 而 a.h 又 #include 了 b.h 档（原因是 b.h 档定义了一些 a.h 需要的类型），会发生什么事呢？这时该Ｃ源码文件 #include 了 b.h 两次。因此每一个在 b.h 中的 #define 都发生了两次，每一 个声明发生了两次，等等。理论上，因为它们是完全一样的拷贝， 所以应该不会有什么问题，但在实际应用上，这是不符合Ｃ的语法 的，可能在编译时出现错误，或至少是警告。 <BR><BR>　　解决的方法是要确定每一个 header 档在任一个源码文件中只被包 含了一次。我们一般是用预处理器来达到这个目的的。当我们进入 每一个 header 档时，我们为这个 header 档 #define 一个巨集 指令。只有在这个巨集指令没有被定义的前提下，我们才真正使用 该 header 档的主体。在实际应用上，我们只要简单的把下面一段 码放在每一个 header 档的开始部分： <BR>　　#ifndef FILENAME_H <BR>　　#define FILENAME_H <BR>　　然后把下面一行码放在最后： <BR>　　#endif <BR>　　用 header 档的档名（大写的）代替上面的 FILENAME_H，用底线 代替档名中的点。有些人喜欢在 #endif 加上注释来提醒他们这个 #endif 指的是什么。例如： <BR>　　#endif /* #ifndef FILENAME_H */ <BR>　　我个人没有这个习惯，因为这其实是很明显的。当然这只是各人的 风格不同，无伤大雅。 <BR>　　你只需要在那些有编译错误的 header 档中加入这个技巧，但在所 有的 header 档中都加入也没什么损失，到底这是个好习惯。 <BR>　　1.5 重新编译一个多文件项目 <BR>　　清楚的区别编译和连接是很重要的。编译器使用源码文件来产生某种 形式的目标文件(object files)。在这个过程中，外部的符号参考并 没有被解释或替换。然后我们使用连接器来连接这些目标文件和一些 标准的程序包再加你指定的程序包，最后连接生成一个可执行程序。 在这个阶段，一个目标文件中对别的文件中的符号的参考被解释，并 报告不能被解释的参考，一般是以错误信息的形式报告出来。 <BR>　　基本的步骤就应该是，把你的源码文件一个一个的编译成目标文件的格 式，最后把所有的目标文件加上需要的程序包连接成一个可执行文件。 具体怎么做是由你的编译器决定的。这里我只给出 gcc （GNU C 编译 器）的有关命令，这些有可能对你的非 gcc 编译器也适用。 <BR>　　gcc 是一个多目标的工具。它在需要的时候呼叫其它的元件（预处理 程序，编译器，组合程序，连接器）。具体的哪些元件被呼叫取决于 输入文件的类型和你传递给它的开关。 <BR>　　一般来说，如果你只给它Ｃ源码文件，它将预处理，编译，组合所有 的文件，然后把所得的目标文件连接成一个可执行文件（一般生成的 文件被命名为 a.out ）。你当然可以这么做，但这会破坏很多我们 把一个项目分解成多个文件所得到的好处。 <BR>　　如果你给它一个 -c 开关，gcc 只把给它的文件编译成目标文件， 用源码文件的文件名命名但把其后缀由“.c”或“.cc”变成“.o”。 如果你给它的是一列目标文件， gcc 会把它们连接成可执行文件， 缺省文件名是 a.out 。你可以改变缺省名，用开关 -o 后跟你指定 的文件名。 <BR><BR>　　因此，当你改变了一个源码文件后，你需要重新编译它： gcc -c filename.c 然后重新连接你的项目： gcc -o exec_filename *.o。 如果你改变了一个 header 档，你需要重新编译所有 #include 过 这个档的源码文件，你可以用 gcc -c file1.c file2.c file3.c 然后象上边一样连接。 <BR><BR>　　当然这么做是很繁琐的，幸亏我们有些工具使这个步骤变得简单。 本文的第二部分就是介绍其中的一件工具：GNU Make 工具。 <BR><BR>　　（好家伙，现在才开始见真章。您学到点儿东西没？） <BR><BR><BR>　　2) GNU Make 工具 <BR>　　~~~~~~~~~~~~~~~~ <BR><BR>　　2.1 基本 makefile 结构 <BR><BR>　　GNU Make 的主要工作是读进一个文本文件， makefile 。这个文 件里主要是有关哪些文件（‘target’目的文件）是从哪些别的 文件（‘dependencies’依靠文件）中产生的，用什么命令来进行 这个产生过程。有了这些信息， make 会检查磁碟上的文件，如果 目的文件的时间戳（该文件生成或被改动时的时间）比至少它的一 个依靠文件旧的话， make 就执行相应的命令，以便更新目的文件。 （目的文件不一定是最后的可执行档，它可以是任何一个文件。） <BR><BR>　　makefile 一般被叫做“makefile”或“Makefile”。当然你可以 在 make 的命令行指定别的文件名。如果你不特别指定，它会寻 找“makefile”或“Makefile”，因此使用这两个名字是最简单 的。 <BR><BR>　　一个 makefile 主要含有一系列的规则，如下： <BR><BR>　　: ... <BR>　　(tab)<COMMAND> <BR>　　(tab)<COMMAND> <BR>　　. <BR>　　. <BR>　　. <BR><BR>　　例如，考虑以下的 makefile ： <BR><BR>　　=== makefile 开始 === <BR>　　myprog : foo.o bar.o <BR>　　gcc foo.o bar.o -o myprog <BR>　　foo.o : foo.c foo.h bar.h <BR>　　gcc -c foo.c -o foo.o <BR>　　bar.o : bar.c bar.h <BR>　　gcc -c bar.c -o bar.o <BR>　　=== makefile 结束 === <BR>　　这是一个非常基本的 makefile —— make 从最上面开始，把上 面第一个目的，‘myprog’，做为它的主要目标（一个它需要保 证其总是最新的最终目标）。给出的规则说明只要文件‘myprog’ 比文件‘foo.o’或‘bar.o’中的任何一个旧，下一行的命令将 会被执行。 <BR><BR>　　但是，在检查文件 foo.o 和 bar.o 的时间戳之前，它会往下查 找那些把 foo.o 或 bar.o 做为目标文件的规则。它找到的关于 foo.o 的规则，该文件的依靠文件是 foo.c, foo.h 和 bar.h 。 它从下面再找不到生成这些依靠文件的规则，它就开始检查磁碟 上这些依靠文件的时间戳。如果这些文件中任何一个的时间戳比 foo.o 的新，命令 gcc -o foo.o foo.c 将会执行，从而更新 文件 foo.o 。 <BR><BR>　　接下来对文件 bar.o 做类似的检查，依靠文件在这里是文件 bar.c 和 bar.h 。 <BR><BR>　　现在， make 回到‘myprog’的规则。如果刚才两个规则中的任 何一个被执行，myprog 就需要重建（因为其中一个 .o 档就会比 ‘myprog’新），因此连接命令将被执行。 <BR><BR>　　希望到此，你可以看出使用 make 工具来建立程序的好处——前 一章中所有繁琐的检查步骤都由 make 替你做了：检查时间戳。 你的源码文件里一个简单改变都会造成那个文件被重新编译（因 为 .o 文件依靠 .c 文件），进而可执行文件被重新连接（因为 .o 文件被改变了）。其实真正的得益是在当你改变一个 header 档的时候——你不再需要记住那个源码文件依靠它，因为所有的 资料都在 makefile 里。 make 会很轻松的替你重新编译所有那 些因依靠这个 header 文件而改变了的源码文件，如有需要，再 进行重新连接。 <BR><BR>　　当然，你要确定你在 makefile 中所写的规则是正确无误的，只 列出那些在源码文件中被 #include 的 header 档…… <BR><BR>　　2.2 编写 make 规则 (Rules) <BR><BR>　　最明显的（也是最简单的）编写规则的方法是一个一个的查 看源码文件，把它们的目标文件做为目的，而Ｃ源码文件和被它 #include 的 header 档做为依靠文件。但是你也要把其它被这些 header 档 #include 的 header 档也列为依靠文件，还有那些被 包括的文件所包括的文件……然后你会发现要对越来越多的文件 进行管理，然后你的头发开始脱落，你的脾气开始变坏，你的脸 色变成菜色，你走在路上开始跟电线杆子碰撞，终于你捣毁你的 电脑显示器，停止编程。到低有没有些容易点儿的方法呢？ <BR><BR>　　当然有！向编译器要！在编译每一个源码文件的时候，它实在应 该知道应该包括什么样的 header 档。使用 gcc 的时候，用 -M 开关，它会为每一个你给它的Ｃ文件输出一个规则，把目标文件 做为目的，而这个Ｃ文件和所有应该被 #include 的 header 文 件将做为依靠文件。注意这个规则会加入所有 header 文件，包 括被角括号(`&lt;, `&gt;)和双引号(`")所包围的文件。其实我们可以 相当肯定系统 header 档（比如 stdio.h, stdlib.h 等等）不会 被我们更改，如果你用 -MM 来代替 -M 传递给 gcc，那些用角括 号包围的 header 档将不会被包括。（这会节省一些编译时间） <BR>　　由 gcc 输出的规则不会含有命令部分；你可以自己写入你的命令 或者什么也不写，而让 make 使用它的隐含的规则（参考下面的 2.4 节）。 <BR>　　2.3 Makefile 变量 <BR>　　上面提到 makefiles 里主要包含一些规则。它们包含的其它的东 西是变量定义。 <BR>　　makefile 里的变量就像一个环境变量(environment variable)。 事实上，环境变量在 make 过程中被解释成 make 的变量。这些 变量是大小写敏感的，一般使用大写字母。它们可以从几乎任何 地方被引用，也可以被用来做很多事情，比如： <BR>　　i) 贮存一个文件名列表。在上面的例子里，生成可执行文件的 规则包含一些目标文件名做为依靠。在这个规则的命令行 里同样的那些文件被输送给 gcc 做为命令参数。如果在这 里使用一个变数来贮存所有的目标文件名，加入新的目标 文件会变的简单而且较不易出错。 <BR><BR>　　ii) 贮存可执行文件名。如果你的项目被用在一个非 gcc 的系 统里，或者如果你想使用一个不同的编译器，你必须将所 有使用编译器的地方改成用新的编译器名。但是如果使用一 个变量来代替编译器名，那么你只需要改变一个地方，其 它所有地方的命令名就都改变了。 <BR><BR>　　iii) 贮存编译器旗标。假设你想给你所有的编译命令传递一组 相同的选项（例如 -Wall -O -g）；如果你把这组选项存 入一个变量，那么你可以把这个变量放在所有呼叫编译器 的地方。而当你要改变选项的时候，你只需在一个地方改 变这个变量的内容。 <BR><BR>　　要设定一个变量，你只要在一行的开始写下这个变量的名字，后 面跟一个 = 号，后面跟你要设定的这个变量的值。以后你要引用 这个变量，写一个 $ 符号，后面是围在括号里的变量名。比如在 下面，我们把前面的 makefile 利用变量重写一遍： <BR><BR>　　=== makefile 开始 === <BR>　　OBJS = foo.o bar.o <BR>　　CC = gcc <BR>　　CFLAGS = -Wall -O -g <BR><BR>　　myprog : $(OBJS) <BR>　　$(CC) $(OBJS) -o myprog <BR><BR>　　foo.o : foo.c foo.h bar.h <BR>　　$(CC) $(CFLAGS) -c foo.c -o foo.o <BR><BR>　　bar.o : bar.c bar.h <BR>　　$(CC) $(CFLAGS) -c bar.c -o bar.o <BR>　　=== makefile 结束 === <BR>　　还有一些设定好的内部变量，它们根据每一个规则内容定义。三个 比较有用的变量是 $@, $&lt; 和 $^ （这些变量不需要括号括住）。 $@ 扩展成当前规则的目的文件名， $&lt; 扩展成依靠列表中的第 一个依靠文件，而 $^ 扩展成整个依靠的列表（除掉了里面所有重 复的文件名）。利用这些变量，我们可以把上面的 makefile 写成： <BR>　　=== makefile 开始 === <BR>　　OBJS = foo.o bar.o <BR>　　CC = gcc <BR>　　CFLAGS = -Wall -O -g <BR>　　myprog : $(OBJS) <BR>　　$(CC) $^ -o $@ <BR>　　foo.o : foo.c foo.h bar.h <BR>　　$(CC) $(CFLAGS) -c $&lt; -o $@ <BR>　　bar.o : bar.c bar.h <BR>　　$(CC) $(CFLAGS) -c $&lt; -o $@ <BR>　　=== makefile 结束 === <BR><BR>　　你可以用变量做许多其它的事情，特别是当你把它们和函数混合 使用的时候。如果需要更进一步的了解，请参考 GNU Make 手册。 (man make, man makefile) <BR><BR>　　2.4 隐含规则 (Implicit Rules) <BR><BR>　　请注意，在上面的例子里，几个产生 .o 文件的命令都是一样的。 都是从 .c 文件和相关文件里产生 .o 文件，这是一个标准的步 骤。其实 make 已经知道怎么做——它有一些叫做隐含规则的内 置的规则，这些规则告诉它当你没有给出某些命令的时候，应该 怎么办。 <BR><BR>　　如果你把生成 foo.o 和 bar.o 的命令从它们的规则中删除， make 将会查找它的隐含规则，然后会找到一个适当的命令。它的命令会 使用一些变量，因此你可以按照你的想法来设定它：它使用变量 CC 做为编译器（象我们在前面的例子），并且传递变量 CFLAGS （给 C 编译器，C++ 编译器用 CXXFLAGS ），CPPFLAGS （ C 预 处理器旗标）， TARGET_ARCH （现在不用考虑这个），然后它加 入旗标 -c ，后面跟变量 $&lt; （第一个依靠名），然后是旗 标 -o 跟变量 $@ （目的文件名）。一个Ｃ编译的具体命令将 会是： <BR><BR>　　$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $&lt; -o $@ <BR><BR>　　当然你可以按照你自己的需要来定义这些变量。这就是为什么用 gcc 的 -M 或 -MM 开关输出的码可以直接用在一个 makefile 里。 <BR><BR>　　2.5 假象目的 (Phony Targets) <BR><BR>　　假设你的一个项目最后需要产生两个可执行文件。你的主要目标 是产生两个可执行文件，但这两个文件是相互独立的——如果一 个文件需要重建，并不影响另一个。你可以使用“假象目的”来 达到这种效果。一个假象目的跟一个正常的目的几乎是一样的， 只是这个目的文件是不存在的。因此， make 总是会假设它需要 被生成，当把它的依赖文件更新后，就会执行它的规则里的命令 行。 <BR>　　如果在我们的 makefile 开始处输入： <BR>　　all : exec1 exec2 <BR>　　其中 exec1 和 exec2 是我们做为目的的两个可执行文件。 make 把这个 all 做为它的主要目的，每次执行时都会尝试把 all 更新。但既然这行规则里没有哪个命令来作用在一个叫 all 的 实际文件（事实上 all 并不会在磁碟上实际产生），所以这个规 则并不真的改变 all 的状态。可既然这个文件并不存在，所以 make 会尝试更新 all 规则，因此就检查它的依靠 exec1, exec2 是否需要更新，如果需要，就把它们更新，从而达到我们的目的。 <BR><BR>　　假象目的也可以用来描述一组非预设的动作。例如，你想把所有由 make 产生的文件删除，你可以在 makefile 里设立这样一个规则： <BR><BR>　　veryclean : <BR>　　rm *.o <BR>　　rm myprog <BR><BR>　　前提是没有其它的规则依靠这个 veryclean 目的，它将永远 不会被执行。但是，如果你明确的使用命令 make veryclean ， make 会把这个目的做为它的主要目标，执行那些 rm 命令。 <BR><BR>　　如果你的磁碟上存在一个叫 veryclean 文件，会发生什么事？这 时因为在这个规则里没有任何依靠文件，所以这个目的文件一定是 最新的了（所有的依靠文件都已经是最新的了），所以既使用户明 确命令 make 重新产生它，也不会有任何事情发生。解决方法是标 明所有的假象目的（用 .PHONY），这就告诉 make 不用检查它们 是否存在于磁碟上，也不用查找任何隐含规则，直接假设指定的目 的需要被更新。在 makefile 里加入下面这行包含上面规则的规则： <BR><BR>　　.PHONY : veryclean <BR><BR>　　就可以了。注意，这是一个特殊的 make 规则，make 知道 .PHONY 是一个特殊目的，当然你可以在它的依靠里加入你想用的任何假象 目的，而 make 知道它们都是假象目的。 <BR><BR>　　2.6 函数 (Functions) <BR><BR>　　makefile 里的函数跟它的变量很相似——使用的时候，你用一个 $ 符号跟开括号，函数名，空格后跟一列由逗号分隔的参数，最后 用关括号结束。例如，在 GNU Make 里有一个叫 wildcard 的函 数，它有一个参数，功能是展开成一列所有符合由其参数描述的文 件名，文件间以空格间隔。你可以像下面所示使用这个命令： <BR><BR>　　SOURCES = $(wildcard *.c) <BR>　　这行会产生一个所有以 .c 结尾的文件的列表，然后存入变量 SOURCES 里。当然你不需要一定要把结果存入一个变量。 <BR>　　另一个有用的函数是 patsubst （ patten substitude, 匹配替 换的缩写）函数。它需要３个参数——第一个是一个需要匹配的 式样，第二个表示用什么来替换它，第三个是一个需要被处理的 由空格分隔的字列。例如，处理那个经过上面定义后的变量， <BR>　　OBJS = $(patsubst %.c,%.o,$(SOURCES)) <BR>　　这行将处理所有在 SOURCES 字列中的字（一列文件名），如果它的 结尾是 .c ，就用 .o 把 .c 取代。注意这里的 % 符号将匹 配一个或多个字符，而它每次所匹配的字串叫做一个‘柄’(stem) 。 在第二个参数里， % 被解读成用第一参数所匹配的那个柄。 <BR>　　2.7 一个比较有效的 makefile <BR>　　利用我们现在所学的，我们可以建立一个相当有效的 makefile 。 这个 makefile 可以完成大部分我们需要的依靠检查，不用做太大 的改变就可直接用在大多数的项目里。 <BR>　　首先我们需要一个基本的 makefile 来建我们的程序。我们可以让 它搜索当前目录，找到源码文件，并且假设它们都是属于我们的项 目的，放进一个叫 SOURCES 的变量。这里如果也包含所有的 *.cc 文件，也许会更保险，因为源码文件可能是 C++ 码的。 <BR><BR>　　SOURCES = $(wildcard *.c *.cc) <BR><BR>　　利用 patsubst ，我们可以由源码文件名产生目标文件名，我们需 要编译出这些目标文件。如果我们的源码文件既有 .c 文件，也有 .cc 文件，我们需要使用相嵌的 patsubst 函数呼叫： <BR><BR>　　OBJS = $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCES))) <BR><BR>　　最里面一层 patsubst 的呼叫会对 .cc 文件进行后缀替代，产生的结 果被外层的 patsubst 呼叫处理，进行对 .c 文件后缀的替代。 <BR><BR>　　现在我们可以设立一个规则来建可执行文件： <BR><BR>　　myprog : $(OBJS) <BR>　　gcc -o myprog $(OBJS) <BR><BR>　　进一步的规则不一定需要， gcc 已经知道怎么去生成目标文件 (object files) 。下面我们可以设定产生依靠信息的规则： <BR><BR>　　depends : $(SOURCES) <BR>　　gcc -M $(SOURCES) &gt; depends <BR><BR>　　在这里如果一个叫 depends 的文件不存在，或任何一个源码文件 比一个已存在的 depends 文件新，那么一个 depends 文件会被生 成。depends 文件将会含有由 gcc 产生的关于源码文件的规则（注 意 -M 开关）。现在我们要让 make 把这些规则当做 makefile 档 的一部分。这里使用的技巧很像 C 语言中的 #include 系统——我 们要求 make 把这个文件 include 到 makefile 里，如下： <BR><BR>　　include depends <BR><BR>　　GNU Make 看到这个，检查 depends 目的是否更新了，如果没有， 它用我们给它的命令重新产生 depends 档。然后它会把这组（新） 规则包含进来，继续处理最终目标 myprog 。当看到有关 myprog 的规则，它会检查所有的目标文件是否更新——利用 depends 文件 里的规则，当然这些规则现在已经是更新过的了。 <BR><BR>　　这个系统其实效率很低，因为每当一个源码文件被改动，所有的源码 文件都要被预处理以产生一个新的 depends 文件。而且它也不是 100% 的安全，这是因为当一个 header 档被改动，依靠信息并不会 被更新。但就基本工作来说，它也算相当有用的了。 <BR><BR>　　2.8 一个更好的 makefile <BR><BR>　　这是一个我为我大多数项目设计的 makefile 。它应该可以不需要修 改的用在大部分项目里。我主要把它用在 djgpp 上，那是一个 DOS 版的 gcc 编译器。因此你可以看到执行的命令名、 alleg 程序包、 和 RM -F 变量都反映了这一点。 <BR><BR>　　=== makefile 开始 === <BR><BR>　　###################################### <BR>　　# <BR>　　# Generic makefile <BR>　　# <BR>　　# by George Foot <BR>　　# email: <A style="COLOR: #003793" href="mailto:george.foot@merton.ox.ac.uk">george.foot@merton.ox.ac.uk</A> <BR>　　# <BR>　　# Copyright (c) 1997 George Foot <BR>　　# All rights reserved. <BR>　　# 保留所有版权 <BR>　　# <BR>　　# No warranty, no liability; <BR>　　# you use this at your own risk. <BR>　　# 没保险，不负责 <BR>　　# 你要用这个，你自己担风险 <BR>　　# <BR>　　# You are free to modify and <BR>　　# distribute this without giving <BR>　　# credit to the original author. <BR>　　# 你可以随便更改和散发这个文件 <BR>　　# 而不需要给原作者什么荣誉。 <BR>　　# （你好意思？） <BR>　　# <BR>　　###################################### <BR>　　### Customising <BR>　　# 用户设定 <BR>　　# <BR>　　# Adjust the following if necessary; EXECUTABLE is the target <BR>　　# executables filename, and LIBS is a list of libraries to link in <BR>　　# (e.g. alleg, stdcx, iostr, etc). You can override these on makes <BR>　　# command line of course, if you prefer to do it that way. <BR>　　# <BR>　　# 如果需要，调整下面的东西。 EXECUTABLE 是目标的可执行文件名， LIBS <BR>　　# 是一个需要连接的程序包列表（例如 alleg, stdcx, iostr 等等）。当然你 <BR>　　# 可以在 make 的命令行覆盖它们，你愿意就没问题。 <BR>　　# <BR>　　EXECUTABLE := mushroom.exe <BR>　　LIBS := alleg <BR>　　# Now alter any implicit rules variables if you like, e.g.: <BR>　　# <BR>　　# 现在来改变任何你想改动的隐含规则中的变量，例如 <BR>　　CFLAGS := -g -Wall -O3 -m486 <BR>　　CXXFLAGS := $(CFLAGS) <BR>　　# The next bit checks to see whether rm is in your djgpp bin <BR>　　# directory; if not it uses del instead, but this can cause (harmless) <BR>　　# `File not found error messages. If you are not using DOS at all, <BR>　　# set the variable to something which will unquestioningly remove <BR>　　# files. <BR>　　# <BR>　　# 下面先检查你的 djgpp 命令目录下有没有 rm 命令，如果没有，我们使用 <BR>　　# del 命令来代替，但有可能给我们 File not found 这个错误信息，这没 <BR>　　# 什么大碍。如果你不是用 DOS ，把它设定成一个删文件而不废话的命令。 <BR>　　# （其实这一步在 UNIX 类的系统上是多余的，只是方便 DOS 用户。 UNIX <BR>　　# 用户可以删除这５行命令。） <BR>　　ifneq ($(wildcard $(DJDIR)/bin/rm.exe),) <BR>　　RM-F := rm -f <BR>　　else <BR>　　RM-F := del <BR>　　endif <BR>　　# You shouldn need to change anything below this point. <BR>　　# <BR>　　# 从这里开始，你应该不需要改动任何东西。（我是不太相信，太ＮＢ了！） <BR>　　SOURCE := $(wildcard *.c) $(wildcard *.cc) <BR>　　OBJS := $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCE))) <BR>　　DEPS := $(patsubst %.o,%.d,$(OBJS)) <BR>　　MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS)) <BR>　　MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS)) \ <BR>　　$(patsubst %.d,%.cc,$(MISSING_DEPS))) <BR>　　CPPFLAGS += -MD <BR>　　.PHONY : everything deps objs clean veryclean rebuild <BR>　　everything : $(EXECUTABLE) <BR>　　deps : $(DEPS) <BR>　　objs : $(OBJS) <BR>　　clean : <BR>　　@$(RM-F) *.o <BR>　　@$(RM-F) *.d <BR>　　veryclean: clean <BR>　　@$(RM-F) $(EXECUTABLE) <BR>　　rebuild: veryclean everything <BR>　　ifneq ($(MISSING_DEPS),) <BR>　　$(MISSING_DEPS) : <BR>　　@$(RM-F) $(patsubst %.d,%.o,$@) <BR>　　endif <BR>　　-include $(DEPS) <BR>　　$(EXECUTABLE) : $(OBJS) <BR>　　gcc -o $(EXECUTABLE) $(OBJS) $(addprefix -l,$(LIBS)) <BR>　　=== makefile 结束 === <BR>　　有几个地方值得解释一下的。首先，我在定义大部分变量的时候使 用的是 := 而不是 = 符号。它的作用是立即把定义中参考到的函 数和变量都展开了。如果使用 = 的话，函数和变量参考会留在那 儿，就是说改变一个变量的值会导致其它变量的值也被改变。例 如： <BR>　　A = foo <BR>　　B = $(A) <BR>　　# 现在 B 是 $(A) ，而 $(A) 是 foo 。 <BR>　　A = bar <BR>　　# 现在 B 仍然是 $(A) ，但它的值已随着变成 ar 了。 <BR>　　B := $(A) <BR>　　# 现在 B 的值是 ar 。 <BR>　　A = foo <BR>　　# B 的值仍然是 ar 。 <BR>　　make 会忽略在 # 符号后面直到那一行结束的所有文字。 <BR>　　ifneg...else...endif 系统是 makefile 里让某一部分码有条件的 失效／有效的工具。 ifeq 使用两个参数，如果它们相同，它把直 到 else （或者 endif ，如果没有 else 的话）的一段码加进 makefile 里；如果不同，把 else 到 endif 间的一段码加入 makefile （如果有 else ）。 ifneq 的用法刚好相反。 <BR>　　filter-out 函数使用两个用空格分开的列表，它把第二列表中所 有的存在于第一列表中的项目删除。我用它来处理 DEPS 列表，把所 有已经存在的项目都删除，而只保留缺少的那些。 <BR>　　我前面说过， CPPFLAGS 存有用于隐含规则中传给预处理器的一些 旗标。而 -MD 开关类似 -M 开关，但是从源码文件 .c 或 .cc 中 形成的文件名是使用后缀 .d 的（这就解释了我形成 DEPS 变量的 步骤）。DEPS 里提到的文件后来用 -include 加进了 makefile 里，它隐藏了所有因文件不存在而产生的错误信息。 <BR>　　如果任何依靠文件不存在， makefile 会把相应的 .o 文件从磁碟 上删除，从而使得 make 重建它。因为 CPPFLAGS 指定了 -MD ， 它的 .d 文件也被重新产生。 <BR>　　最后， addprefix 函数把第二个参数列表的每一项前缀上第一 个参数值。 <BR>　　这个 makefile 的那些目的是（这些目的可以传给 make 的命令行 来直接选用）： <BR>　　everything:（预设） 更新主要的可执行程序，并且为每一个 源码文件生成或更新一个 .d 文件和一个 .o 文件。 <BR>　　deps: 只是为每一个源码程序产生或更新一个 .d 文件。 <BR>　　objs: 为每一个源码程序生成或更新 .d 文件和目标文件。 <BR>　　clean: 删除所有中介／依靠文件（ *.d 和 *.o ）。 <BR>　　veryclean: 做 `clean 和删除可执行文件。 <BR>　　rebuild: 先做 `veryclean 然后 `everything ；既完全重建。 <BR>　　除了预设的 everything 以外，这里头只有 clean ， veryclean ， 和 rebuild 对用户是有意义的。 <BR>　　我还没有发现当给出一个源码文件的目录，这个 makefile 会失败的 情况，除非依靠文件被弄乱。如果这种弄乱的情况发生了，只要输入 `make clean ，所有的目标文件和依靠文件会被删除，问题就应该 被解决了。当然，最好不要把它们弄乱。如果你发现在某种情况下这 个 makefile 文件不能完成它的工作，请告诉我，我会把它整好的。 <BR>　　3 总结 <BR>　　~~~~~~~~~~~~~~~ <BR>　　我希望这篇文章足够详细的解释了多文件项目是怎么运作的，也说明了 怎样安全而合理的使用它。到此，你应该可以轻松的利用 GNU Make 工 具来管理小型的项目，如果你完全理解了后面几个部分的话，这些对于 你来说应该没什么困难。 <BR>　　GNU Make 是一件强大的工具，虽然它主要是用来建立程序，它还有很多 别的用处。如果想要知道更多有关这个工具的知识，它的句法，函数， 和许多别的特点，你应该参看它的参考文件 (info pages, 别的 GNU 工具也一样，看它们的 info pages. )。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/488.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:33 <a href="http://www.cnitblog.com/drizztzou/articles/488.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux驱动程式编写</title><link>http://www.cnitblog.com/drizztzou/articles/487.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:31:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/487.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/487.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/487.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/487.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/487.html</trackback:ping><description><![CDATA[&nbsp; 工作需要寫了我們公司一塊網卡的Linux驅動程式。經歷一個從無到有的過程， <BR>深感技術交流的重 <BR><BR>要。Linux作為挑戰微軟壟斷的強有力武器，日益受到大家的喜 <BR>愛。真希望她能在中國迅速成長。把程式文檔貼出來，希望和大家探討Linux技術 <BR>和應用，促進Linux在中國的普及。 <BR>本文可隨意轉載，但請不要在盈利性出版物上刊登。 <BR><BR>------------------ Linux作業系統網路驅動程式編寫 ------------------- <BR>------------ Contact the author by mailto:bordi@bordi.dhs.org ------ <BR><BR>Linux作業系統網路驅動程式編寫 <BR><BR>一.Linux系統設備驅動程式概述 <BR>1.1 Linux設備驅動程式分類 <BR>1.2 編寫驅動程式的一些基本概念 <BR>二.Linux系統網路設備驅動程式 <BR>2.1 網路驅動程式的結構 <BR>2.2 網路驅動程式的基本方法 <BR>2.3 網路驅動程式中用到的資料結構 <BR>2.4 常用的系統支援 <BR>三.編寫Linux網路驅動程式中可能遇到的問題 <BR>3.1 中斷共用 <BR>3.2 硬體發送忙時的處理 <BR>3.3 流量控制(flow control) <BR>3.4 調試 <BR>四.進一步的閱讀 <BR>五.雜項 <BR><BR><BR><BR><BR>一.Linux系統設備驅動程式概述 <BR>1.1 Linux設備驅動程式分類 <BR>Linux設備驅動程式在Linux的內核源代碼中佔有很大的比例，源代碼的長度日 <BR>益增加，主要是驅動程式的增加。在Linux內核的不斷升級過程中，驅動程式的結構 <BR>還是相對穩定。在2.0.xx到2.2.xx的變動裏，驅動程式的編寫做了一些改變，但是 <BR>從2.0.xx的驅動到2.2.xx的移植只需做少量的工作。 <BR><BR>Linux系統的設備分為字元設備(char device)，塊設備(block device)和網路 <BR>設備(network device)三種。字元設備是指存取時沒有緩存的設備。塊設備的讀寫 <BR>都有緩存來支援，並且塊設備必須能夠隨機存取(random access)，字元設備則沒有 <BR>這個要求。典型的字元設備包括滑鼠，鍵盤，串列口等。塊設備主要包括硬碟軟碟 <BR>設備，CD-ROM等。一個檔系統要安裝進入作業系統必須在塊設備上。 <BR><BR>網路設備在Linux裏做專門的處理。Linux的網路系統主要是基於BSD unix的 <BR>socket <BR>機制。在系統和驅動程式之間定義有專門的資料結構(sk_buff)進行資料的傳遞。系 <BR>統裏支援對發送資料和接收資料的緩存，提供流量控制機制，提供對多協定的支援。 <BR><BR><BR><BR>1.2 編寫驅動程式的一些基本概念 <BR>無論是什麼作業系統的驅動程式，都有一些通用的概念。作業系統提供給驅動 <BR>程式的支援也大致相同。下面簡單介紹一下網路設備驅動程式的一些基本要求。 <BR><BR>1.2.1 發送和接收 <BR>這是一個網路設備最基本的功能。一塊網卡所做的無非就是收發工作。所以驅 <BR>動程式裏要告訴系統你的發送函數在哪里，系統在有資料要發送時就會調用你的發 <BR>送程式。還有驅動程式由於是直接操縱硬體的，所以網路硬體有資料收到最先能得 <BR>到這個資料的也就是驅動程式，它負責把這些原始資料進行必要的處理然後送給系 <BR>統。這裏，作業系統必須要提供兩個機制，一個是找到驅動程式的發送函數，一個 <BR>是驅動程式把收到的資料送給系統。 <BR><BR>1.2.2 中斷 <BR>中斷在現代電腦結構中有重要的地位。作業系統必須提供驅動程式回應中斷 <BR>的能力。一般是把一個中斷處理程式註冊到系統中去。作業系統在硬體中斷發生後 <BR>調用驅動程式的處理程式。Linux支援中斷的共用，即多個設備共用一個中斷。 <BR><BR>1.2.3 時鐘 <BR>在實現驅動程式時，很多地方會用到時鐘。如某些協議裏的超時處理，沒有中 <BR>斷機制的硬體的輪詢等。作業系統應為驅動程式提供定時機制。一般是在預定的時 <BR>間過了以後回調註冊的時鐘函數。在網路驅動程式中，如果硬體沒有中斷功能，定 <BR>時器可以提供輪詢(poll)方式對硬體進行存取。或者是實現某些協定時需要的超時 <BR>重傳等。 <BR><BR>二.Linux系統網路設備驅動程式 <BR><BR>2.1 網路驅動程式的結構 <BR>所有的Linux網路驅動程式遵循通用的介面。設計時採用的是面向物件的方法。 <BR>一個設備就是一個物件(device 結構)，它內部有自己的資料和方法。每一個設備的 <BR>方法被調用時的第一個參數都是這個設備物件本身。這樣這個方法就可以存取自身 <BR>的資料(類似面向物件程式設計時的this引用)。 <BR>一個網路設備最基本的方法有初始化、發送和接收。 <BR><BR>------------------- --------------------- <BR>|deliver packets | |receive packets queue| <BR>|(dev_queue_xmit()) | |them(netif_rx()) | <BR>------------------- --------------------- <BR>| | / \ <BR>\ / | | <BR>------------------------------------------------------- <BR>| methods and variables(initialize,open,close,hard_xmit,| <BR>| interrupt handler,config,resources,status...) | <BR>------------------------------------------------------- <BR>| | / \ <BR>\ / | | <BR>----------------- ---------------------- <BR>|send to hardware | |receivce from hardware| <BR>----------------- ---------------------- <BR>| | / \ <BR>\ / | | <BR>----------------------------------------------------- <BR>| hardware media | <BR>----------------------------------------------------- <BR><BR>初始化程式完成硬體的初始化、device中變數的初始化和系統資源的申請。發送 <BR><BR>程式是在驅動程式的上層協定層有資料要發送時自動調用的。一般驅動程式中不對發 <BR><BR>送資料進行緩存，而是直接使用硬體的發送功能把資料發送出去。接收資料一般是通 <BR><BR>過硬體中斷來通知的。在中斷處理程式裏，把硬體幀資訊填入一個skbuff結構中，然 <BR><BR><BR>------------------ Linux作業系統網路驅動程式編寫 ------------------- <BR>------------ Contact the author by mailto:bordi@bordi.dhs.org ------ <BR><BR>後調用netif_rx()傳遞給上層處理。 <BR><BR><BR>2.2 網路驅動程式的基本方法 <BR>網路設備做為一個物件，提供一些方法供系統訪問。正是這些有統一介面的方 <BR>法， <BR>掩蔽了硬體的具體細節，讓系統對各種網路設備的訪問都採用統一的形式，做到硬體 <BR><BR>無關性。 <BR>下面解釋最基本的方法。 <BR>2.2.1 初始化(initialize) <BR>驅動程式必須有一個初始化方法。在把驅動程式載入系統的時候會調用這個初 <BR>始化程式。它做以下幾方面的工作。檢測設備。在初始化程式裏你可以根據硬體的 <BR>特徵檢查硬體是否存在，然後決定是否啟動這個驅動程式。配置和初始化硬體。在 <BR>初始化程式裏你可以完成對硬體資源的配置，比如即插即用的硬體就可以在這個時 <BR>候進行配置(Linux內核對PnP功能沒有很好的支援，可以在驅動程式裏完成這個功 <BR>能)。配置或協商好硬體佔用的資源以後，就可以向系統申請這些資源。有些資源是 <BR>可以和別的設備共用的，如中斷。有些是不能共用的，如IO、DMA。接下來你要初始 <BR>化device結構中的變數。最後，你可以讓硬體正式開始工作。 <BR><BR>2.2.2 打開(open) <BR>open這個方法在網路設備驅動程式裏是網路設備被啟動的時候被調用(即設備狀 <BR>態由down--&gt;up)。所以實際上很多在initialize中的工作可以放到這裏來做。比如資 <BR><BR>源的申請，硬體的啟動。如果dev-&gt;open返回非0(error)，則硬體的狀態還是down。 <BR>open方法另一個作用是如果驅動程式做為一個模組被裝入，則要防止模組卸載時 <BR><BR>設備處於打開狀態。在open方法裏要調用MOD_INC_USE_COUNT宏。 <BR><BR>2.2.3 關閉(stop) <BR>close方法做和open相反的工作。可以釋放某些資源以減少系統負擔。close是在 <BR><BR>設備狀態由up轉為down時被調用的。另外如果是做為模組裝入的驅動程式，close裏 <BR>應該調用MOD_DEC_USE_COUNT，減少設備被引用的次數，以使驅動程式可以被卸載。 <BR>另外close方法必須返回成功(0==success)。 <BR><BR>2.2.4 發送(hard_start_xmit) <BR>所有的網路設備驅動程式都必須有這個發送方法。在系統調用驅動程式的xmit <BR>時，發送的資料放在一個sk_buff結構中。一般的驅動程式把數據傳給硬體發出去。 <BR>也有一些特殊的設備比如loopback把資料組成一個接收資料再回送給系統，或者 <BR>dummy設備直接丟棄資料。 <BR>如果發送成功，hard_start_xmit方法裏釋放sk_buff，返回0(發送成功)。如果 <BR>設備暫時無法處理，比如硬體忙，則返回1。這時如果dev-&gt;tbusy置為非0，則系統 <BR>認為硬體忙，要等到dev-&gt;tbusy置0以後才會再次發送。tbusy的置0任務一般由中斷 <BR>完成。硬體在發送結束後產生中斷，這時可以把tbusy置0，然後用mark_bh()調用通 <BR>知系統可以再次發送。在發送不成功的情況下，也可以不置dev-&gt;tbusy為非0，這樣 <BR>系統會不斷嘗試重發。如果hard_start_xmit發送不成功，則不要釋放sk_buff。 <BR>傳送下來的sk_buff中的資料已經包含硬體需要的幀頭。所以在發送方法裏不需 <BR>要再填充硬體幀頭，資料可以直接提交給硬體發送。sk_buff是被鎖住的(locked)， <BR>確保其他程式不會存取它。 <BR><BR>2.2.5 接收(reception) <BR>驅動程式並不存在一個接收方法。有資料收到應該是驅動程式來通知系統的。 <BR>一般設備收到資料後都會產生一個中斷，在中斷處理程式中驅動程式申請一塊 <BR>sk_buff(skb)，從硬體讀出資料放置到申請好的緩衝區裏。接下來填充sk_buff中 <BR>的一些資訊。skb-&gt;dev = dev，判斷收到幀的協議類型，填入skb-&gt;protocol(多協 <BR>議的支持)。把指針skb-&gt;mac.raw指向硬體資料然後丟棄硬體幀頭(skb_pull)。還要 <BR>設置skb-&gt;pkt_type，標明第二層(鏈路層)資料類型。可以是以下類型： <BR>PACKET_BROADCAST : 鏈路層廣播 <BR>PACKET_MULTICAST : 鏈路層組播 <BR>PACKET_SELF : 發給自己的幀 <BR>PACKET_OTHERHOST : 發給別人的幀(監聽模式時會有這種幀) <BR><BR>最後調用netif_rx()把資料傳送給協定層。netif_rx()裏資料放入處理佇列然後返 <BR>回，真正的處理是在中斷返回以後，這樣可以減少中斷時間。調用netif_rx()以後， <BR><BR>驅動程式就不能再存取資料緩衝區skb。 <BR><BR>2.2.6 硬體幀頭(hard_header) <BR>硬體一般都會在上層資料發送之前加上自己的硬體幀頭，比如乙太網(Ethernet) <BR><BR>就有14位元組的幀頭。這個幀頭是加在上層ip、ipx等數據包的前面的。驅動程式提供 <BR>一個hard_header方法，協議層(ip、ipx、arp等)在發送資料之前會調用這段程式。 <BR>硬體幀頭的長度必須填在dev-&gt;hard_header_len，這樣協定層回在資料之前保留好 <BR>硬體幀頭的空間。這樣hard_header程式只要調用skb_push然後正確填入硬體幀頭就 <BR>可以了。 <BR>在協議層調用hard_header時，傳送的參數包括(2.0.xx)：數據的sk_buff， <BR>device指針，protocol，目的地址(daddr)，源位址(saddr)，資料長度(len)。數據 <BR>長度不要使用sk_buff中的參數，因為調用hard_header時資料可能還沒完全組織好。 <BR><BR>saddr是NULL的話是使用缺省位址(default)。daddr是NULL表明協定層不知道硬體目 <BR>的地址。如果hard_header完全填好了硬體幀頭，則返回添加的位元組數。如果硬體幀 <BR>頭中的資訊還不完全(比如daddr為NULL，但是幀頭中需要目的硬體位址。典型的情 <BR>況是乙太網需要位址解析(arp))，則返回負位元組數。hard_header返回負數的情況 <BR>下，協議層會做進一步的build header的工作。目前Linux系統裏就是做arp <BR>(如果hard_header返回正，dev-&gt;arp=1，表明不需要做arp，返回負，dev-&gt;arp=0， <BR>做arp)。 <BR>對hard_header的調用在每個協定層的處理程式裏。如ip_output。 <BR><BR>2.2.7 地址解析(xarp) <BR>有些網路有硬體位址(比如Ethernet)，並且在發送硬體幀時需要知道目的硬體 <BR>地址。這樣就需要上層協定位址(ip、ipx)和硬體位址的對應。這個對應是通過位址 <BR>解析完成的。需要做arp的的設備在發送之前會調用驅動程式的rebuild_header方 <BR>法。調用的主要參數包括指向硬體幀頭的指標，協議層位址。如果驅動程式能夠解 <BR>析硬體位址，就返回1，如果不能，返回0。 <BR>對rebuild_header的調用在net/core/dev.c的do_dev_queue_xmit()裏。 <BR><BR>2.2.8 參數設置和統計資料 <BR>在驅動程式裏還提供一些方法供系統對設備的參數進行設置和讀取資訊。一般 <BR>只有超級用戶(root)許可權才能對設備參數進行設置。設置方法有： <BR>dev-&gt;set_mac_address() <BR>當用戶調用ioctl類型為SIOCSIFHWADDR時是要設置這個設備的mac位址。一般 <BR>對mac位址的設置沒有太大意義的。 <BR>dev-&gt;set_config() <BR><BR>------------------ Linux作業系統網路驅動程式編寫 ------------------- <BR>------------ Contact the author by mailto:bordi@bordi.dhs.org ------ <BR><BR>當用戶調用ioctl時類型為SIOCSIFMAP時，系統會調用驅動程式的set_config <BR>方法。用戶會傳遞一個ifmap結構包含需要的I/O、中斷等參數。 <BR>dev-&gt;do_ioctl() <BR>如果用戶調用ioctl時類型在SIOCDEVPRIVATE和SIOCDEVPRIVATE+15之間，系統 <BR>會調用驅動程式的這個方法。一般是設置設備的專用資料。 <BR>讀取資訊也是通過ioctl調用進行。除次之外驅動程式還可以提供一個 <BR>dev-&gt;get_stats方法，返回一個enet_statistics結構，包含發送接收的統計資訊。 <BR>ioctl的處理在net/core/dev.c的dev_ioctl()和dev_ifsioc()裏。 <BR><BR><BR>2.3 網路驅動程式中用到的資料結構 <BR>最重要的是網路設備的資料結構。定義在include/linux/netdevice.h裏。它 <BR>的注釋已經足夠詳盡。 <BR>struct device <BR>{ <BR><BR>/* <BR>* This is the first field of the "visible" part of this structure <BR>* (i.e. as seen by users in the "Space.c" file). It is the name <BR>* the interface. <BR>*/ <BR>char *name; <BR><BR>/* I/O specific fields - FIXME: Merge these and struct ifmap into one */ <BR><BR>unsigned long rmem_end; /* shmem "recv" end */ <BR><BR>unsigned long rmem_start; /* shmem "recv" start */ <BR><BR>unsigned long mem_end; /* shared mem end */ <BR><BR>unsigned long mem_start; /* shared mem start */ <BR><BR>unsigned long base_addr; /* device I/O address */ <BR><BR>unsigned char irq; /* device IRQ number */ <BR><BR><BR>/* Low-level status flags. */ <BR>volatile unsigned char start, /* start an operation */ <BR><BR>interrupt; /* interrupt arrived */ <BR><BR>/* 在處理中斷時interrupt設為1，處理完清0。 */ <BR>unsigned long tbusy; /* transmitter busy must <BR>be long for <BR>bitops */ <BR><BR>struct device *next; <BR><BR>/* The device initialization function. Called only once. */ <BR>/* 指向驅動程式的初始化方法。 */ <BR>int (*init)(struct device *dev); <BR><BR>/* Some hardware also needs these fields, but they are not part of the <BR>usual set specified in Space.c. */ <BR>/* 一些硬體可以在一塊板上支援多個介面，可能用到if_port。 */ <BR>unsigned char if_port; /* Selectable AUI, TP,..*/ <BR><BR>unsigned char dma; /* DMA channel */ <BR><BR><BR>struct enet_statistics* (*get_stats)(struct device *dev); <BR><BR>/* <BR>* This marks the end of the "visible" part of the structure. All <BR>* fields hereafter are internal to the system, and may change at <BR>* will (read: may be cleaned up at will). <BR>*/ <BR><BR>/* These may be needed for future network-power-down code. */ <BR>/* trans_start記錄最後一次成功發送的時間。可以用來確定硬體是否工作正常。 <BR>*/ <BR>unsigned long trans_start; /* Time (in jiffies) of last Tx */ <BR><BR>unsigned long last_rx; /* Time of last Rx */ <BR><BR><BR>/* flags裏面有很多內容，定義在include/linux/if.h裏。*/ <BR>unsigned short flags; /* interface flags (a la BSD) */ <BR><BR>unsigned short family; /* address family ID (AF_INET) */ <BR><BR>unsigned short metric; /* routing metric (not used) */ <BR><BR>unsigned short mtu; /* interface MTU value */ <BR><BR><BR>/* type標明物理硬體的類型。主要說明硬體是否需要arp。定義在 <BR>include/linux/if_arp.h裏。 */ <BR>unsigned short type; /* interface hardware type */ <BR><BR><BR>/* 上層協定層根據hard_header_len在發送資料緩衝區前面預留硬體幀頭空間。*/ <BR><BR>unsigned short hard_header_len; /* hardware hdr length */ <BR><BR><BR>/* priv指向驅動程式自己定義的一些參數。*/ <BR>void *priv; /* pointer to private data */ <BR><BR><BR>/* Interface address info. */ <BR>unsigned char broadcast[MAX_ADDR_LEN]; /* hw bcast add */ <BR><BR>unsigned char pad; /* make dev_addr <BR>aligned to 8 <BR>bytes */ <BR>unsigned char dev_addr[MAX_ADDR_LEN]; /* hw address */ <BR><BR>unsigned char addr_len; /* hardware address length */ <BR><BR>unsigned long pa_addr; /* protocol address */ <BR><BR>unsigned long pa_brdaddr; /* protocol broadcast addr */ <BR><BR>unsigned long pa_dstaddr; /* protocol P-P other side addr */ <BR><BR>unsigned long pa_mask; /* protocol netmask */ <BR><BR>unsigned short pa_alen; /* protocol address length */ <BR><BR><BR>struct dev_mc_list *mc_list; /* Multicast mac addresses */ <BR><BR>int mc_count; /* Number of installed mcasts */ <BR><BR><BR>struct ip_mc_list *ip_mc_list; /* IP multicast filter chain */ <BR><BR>__u32 tx_queue_len; /* Max frames per queue allowed */ <BR><BR><BR><BR>------------------ Linux作業系統網路驅動程式編寫 ------------------- <BR>------------ Contact the author by mailto:bordi@bordi.dhs.org ------ <BR><BR>/* For load balancing driver pair support */ <BR><BR>unsigned long pkt_queue; /* Packets queued */ <BR>struct device *slave; /* Slave device */ <BR>struct net_alias_info *alias_info; /* main dev alias info */ <BR>struct net_alias *my_alias; /* alias devs */ <BR><BR>/* Pointer to the interface buffers. */ <BR>struct sk_buff_head buffs[DEV_NUMBUFFS]; <BR><BR>/* Pointers to interface service routines. */ <BR>int (*open)(struct device *dev); <BR>int (*stop)(struct device *dev); <BR>int (*hard_start_xmit) (struct sk_buff *skb, <BR>struct device *dev); <BR>int (*hard_header) (struct sk_buff *skb, <BR>struct device *dev, <BR>unsigned short type, <BR>void *daddr, <BR>void *saddr, <BR>unsigned len); <BR>int (*rebuild_header)(void *eth, struct device *dev, <BR><BR>unsigned long raddr, struct sk_buff *skb); <BR><BR>#define HAVE_MULTICAST <BR>void (*set_multicast_list)(struct device *dev); <BR>#define HAVE_SET_MAC_ADDR <BR>int (*set_mac_address)(struct device *dev, void <BR>*addr); <BR>#define HAVE_PRIVATE_IOCTL <BR>int (*do_ioctl)(struct device *dev, struct ifreq <BR>*ifr, int cmd); <BR>#define HAVE_SET_CONFIG <BR>int (*set_config)(struct device *dev, struct ifmap <BR>*map); <BR>#define HAVE_HEADER_CACHE <BR>void (*header_cache_bind)(struct hh_cache **hhp, <BR>struct device <BR>*dev, unsigned short htype, __u32 daddr); <BR>void (*header_cache_update)(struct hh_cache *hh, <BR>struct device <BR>*dev, unsigned char * haddr); <BR>#define HAVE_CHANGE_MTU <BR>int (*change_mtu)(struct device *dev, int new_mtu); <BR><BR>struct iw_statistics* (*get_wireless_stats)(struct device *dev); <BR>}; <BR><BR><BR>2.4 常用的系統支援 <BR><BR>2.4.1 記憶體申請和釋放 <BR>include/linux/kernel.h裏聲明了kmalloc()和kfree()。用於在內核模式下申 <BR>請和釋放記憶體。 <BR>void *kmalloc(unsigned int len,int priority); <BR>void kfree(void *__ptr); <BR>與用戶模式下的malloc()不同，kmalloc()申請空間有大小限制。長度是2的整 <BR>次方。可以申請的最大長度也有限制。另外kmalloc()有priority參數，通常使用 <BR>時可以為GFP_KERNEL，如果在中斷裏調用用GFP_ATOMIC參數，因為使用GFP_KERNEL <BR>則調用者可能進入sleep狀態，在處理中斷時是不允許的。 <BR>kfree()釋放的記憶體必須是kmalloc()申請的。如果知道記憶體的大小，也可以用 <BR>kfree_s()釋放。 <BR><BR>2.4.2 request_irq()、free_irq() <BR>這是驅動程式申請中斷和釋放中斷的調用。在include/linux/sched.h裏聲明。 <BR>request_irq()調用的定義： <BR>int request_irq(unsigned int irq, <BR>void (*handler)(int irq, void *dev_id, struct pt_regs <BR>*regs), <BR>unsigned long irqflags, <BR>const char * devname, <BR>void *dev_id); <BR>irq是要申請的硬體中斷號。在Intel平臺，範圍0--15。handler是向系統登記 <BR>的中斷處理函數。這是一個回調函數，中斷發生時，系統調用這個函數，傳入的參 <BR>數包括硬體中斷號，device id，寄存器值。dev_id就是下面的request_irq時傳遞 <BR>給系統的參數dev_id。irqflags是中斷處理的一些屬性。比較重要的有 <BR>SA_INTERRUPT， <BR>標明中斷處理程式是快速處理程式(設置SA_INTERRUPT)還是慢速處理程式(不設置 <BR>SA_INTERRUPT)。快速處理程式被調用時遮罩所有中斷。慢速處理程式不遮罩。還有 <BR>一個SA_SHIRQ屬性，設置了以後運行多個設備共用中斷。dev_id在中斷共用時會用 <BR>到。一般設置為這個設備的device結構本身或者NULL。中斷處理程式可以用dev_id <BR>找到相應的控制這個中斷的設備，或者用irq2dev_map找到中斷對應的設備。 <BR>void free_irq(unsigned int irq,void *dev_id); <BR><BR>2.4.3 時鐘 <BR>時鐘的處理類似中斷，也是登記一個時間處理函數，在預定的時間過後，系統 <BR>會調用這個函數。在include/linux/timer.h裏聲明。 <BR>struct timer_list { <BR>struct timer_list *next; <BR>struct timer_list *prev; <BR>unsigned long expires; <BR>unsigned long data; <BR>void (*function)(unsigned long); <BR>}; <BR>void add_timer(struct timer_list * timer); <BR>int del_timer(struct timer_list * timer); <BR>void init_timer(struct timer_list * timer); <BR>使用時鐘，先聲明一個timer_list結構，調用init_timer對它進行初始化。 <BR>time_list結構裏expires是標明這個時鐘的週期，單位採用jiffies的單位。 <BR>jiffies是Linux一個總體變數，代表時間。它的單位隨硬體平臺的不同而不同。 <BR>系統裏定義了一個常數HZ，代表每秒種最小時間間隔的數目。這樣jiffies的單位 <BR>就是1/HZ。Intel平臺jiffies的單位是1/100秒，這就是系統所能分辨的最小時間 <BR>間隔了。所以expires/HZ就是以秒為單位的這個時鐘的週期。 <BR>function就是時間到了以後的回調函數，它的參數就是timer_list中的data。 <BR>data這個參數在初始化時鐘的時候賦值，一般賦給它設備的device結構指標。 <BR>在預置時間到系統調用function，同時系統把這個time_list從定時佇列裏清 <BR>除。所以如果需要一直使用定時函數，要在function裏再次調用add_timer()把這 <BR><BR>------------------ Linux作業系統網路驅動程式編寫 ------------------- <BR>------------ Contact the author by mailto:bordi@bordi.dhs.org ------ <BR><BR>個timer_list加進定時佇列。 <BR><BR>2.4.4 I/O <BR>I/O埠的存取使用： <BR>inline unsigned int inb(unsigned short port); <BR>inline unsigned int inb_p(unsigned short port); <BR>inline void outb(char value, unsigned short port); <BR>inline void outb_p(char value, unsigned short port); <BR>在include/adm/io.h裏定義。 <BR>inb_p()、outb_p()與inb()、outb_p()的不同在於前者在存取I/O時有等待 <BR>(pause)一適應慢速的I/O設備。 <BR>為了防止存取I/O時發生衝突，Linux提供對埠使用情況的控制。在使用埠 <BR>之前，可以檢查需要的I/O是否正在被使用，如果沒有，則把埠標記為正在使用， <BR>使用完後再釋放。系統提供以下幾個函數做這些工作。 <BR>int check_region(unsigned int from, unsigned int extent); <BR>void request_region(unsigned int from, unsigned int extent,const char <BR>*name); <BR>void release_region(unsigned int from, unsigned int extent); <BR>其中的參數from表示用到的I/O埠的起始位址，extent標明從from開始的端 <BR>口數目。name為設備名稱。 <BR><BR>2.4.5 中斷打開關閉 <BR>系統提供給驅動程式開放和關閉回應中斷的能力。是在include/asm/system.h <BR>中的兩個定義。 <BR>#define cli() __asm__ __volatile__ ("cli"::) <BR>#define sti() __asm__ __volatile__ ("sti"::) <BR><BR>2.4.6 列印資訊 <BR>類似普通程式裏的printf()，驅動程式要輸出資訊使用printk()。在include <BR>/linux/kernel.h裏聲明。 <BR>int printk(const char* fmt, ...); <BR>其中fmt是格式化字串。...是參數。都是和printf()格式一樣的。 <BR><BR>2.4.7 註冊驅動程式 <BR>如果使用模組(module)方式載入驅動程式，需要在模組初始化時把設備註冊 <BR>到系統設備表裏去。不再使用時，把設備從系統中卸除。定義在 <BR>drivers/net/net_init.h <BR>裏的兩個函數完成這個工作。 <BR>int register_netdev(struct device *dev); <BR>void unregister_netdev(struct device *dev); <BR>dev就是要註冊進系統的設備結構指標。在register_netdev()時，dev結構一 <BR>般填寫前面11項，即到init，後面的暫時可以不用初始化。最重要的是name指針和 <BR>init方法。name指標空(NULL)或者內容為\或者name[0]為空格(space)，則系統 <BR>把你的設備做為乙太網設備處理。乙太網設備有統一的命名格式，ethX。對乙太網 <BR>這麼特別對待大概和Linux的歷史有關。 <BR>init方法一定要提供，register_netdev()會調用這個方法讓你對硬體檢測和 <BR>設置。 <BR>register_netdev()返回0表示成功，非0不成功。 <BR><BR>2.4.8 sk_buff <BR>Linux網路各層之間的資料傳送都是通過sk_buff。sk_buff提供一套管理緩衝 <BR>區的方法，是Linux系統網路高效運行的關鍵。每個sk_buff包括一些控制方法和一 <BR>塊數據緩衝區。控制方法按功能分為兩種類型。一種是控制整個buffer鏈的方法， <BR>另一種是控制資料緩衝區的方法。sk_buff組織成雙向鏈表的形式，根據網路應用 <BR>的特點，對鏈表的操作主要是刪除鏈表頭的元素和添加到鏈表尾。sk_buff的控制 <BR>方法都很短小以儘量減少系統負荷。(translated from article written by Alan <BR>Cox) <BR>常用的方法包括： <BR>.alloc_skb() 申請一個sk_buff並對它初始化。返回就是申請到的sk_buff。 <BR>.dev_alloc_skb()類似alloc_skb，在申請好緩衝區後，保留16位元組的幀頭空 <BR>間。主要用在Ethernet驅動程式。 <BR>.kfree_skb() 釋放一個sk_buff。 <BR>.skb_clone() 複製一個sk_buff，但不複製資料部分。 <BR>.skb_copy()完全複製一個sk_buff。 <BR>.skb_dequeue() 從一個sk_buff鏈表裏取出第一個元素。返回取出的sk_buff， <BR>如果鏈表空則返回NULL。這是常用的一個操作。 <BR>.skb_queue_head() 在一個sk_buff鏈表頭放入一個元素。 <BR>.skb_queue_tail() 在一個sk_buff鏈表尾放入一個元素。這也是常用的一個 <BR>操作。網路資料的處理主要是對一個先進先出佇列的管理，skb_queue_tail() <BR>和skb_dequeue()完成這個工作。 <BR>.skb_insert() 在鏈表的某個元素前插入一個元素。 <BR>.skb_append() 在鏈表的某個元素後插入一個元素。一些協議(如TCP)對沒按 <BR>順序到達的資料進行重組時用到skb_insert()和skb_append()。 <BR><BR>.skb_reserve() 在一個申請好的sk_buff的緩衝區裏保留一塊空間。這個空間 <BR>一般是用做下一層協定的頭空間的。 <BR>.skb_put() 在一個申請好的sk_buff的緩衝區裏為資料保留一塊空間。在 <BR>alloc_skb以後，申請到的sk_buff的緩衝區都是處於空(free)狀態，有一個 <BR>tail指標指向free空間，實際上開始時tail就指向緩衝區頭。skb_reserve() <BR>在free空間裏申請協定頭空間，skb_put()申請資料空間。見下麵的圖。 <BR>.skb_push() 把sk_buff緩衝區裏資料空間往前移。即把Head room中的空間移 <BR>一部分到Data area。 <BR>.skb_pull() 把sk_buff緩衝區裏Data area中的空間移一部分到Head room中。 <BR><BR>-------------------------------------------------- <BR>| Tail room(free) | <BR>-------------------------------------------------- <BR>After alloc_skb() <BR><BR>-------------------------------------------------- <BR>| Head room | Tail room(free) | <BR>-------------------------------------------------- <BR>After skb_reserve() <BR><BR>-------------------------------------------------- <BR>| Head room | Data area | Tail room(free) | <BR>-------------------------------------------------- <BR>After skb_put() <BR><BR>-------------------------------------------------- <BR>|Head| skb_ | Data | Tail room(free) | <BR>|room| push | | | <BR>| | Data area | | <BR>-------------------------------------------------- <BR>After skb_push() <BR><BR>-------------------------------------------------- <BR>| Head | skb_ | Data area | Tail room(free) | <BR>| | pull | | | <BR>| Head room | | | <BR>-------------------------------------------------- <BR>After skb_pull() <BR><BR><BR>------------------ Linux作業系統網路驅動程式編寫 ------------------- <BR>------------ Contact the author by mailto:bordi@bordi.dhs.org ------ <BR><BR><BR>三.編寫Linux網路驅動程式中需要注意的問題 <BR><BR>3.1 中斷共用 <BR>Linux系統運行幾個設備共用同一個中斷。需要共用的話，在申請的時候指明 <BR>共用方式。系統提供的request_irq()調用的定義： <BR>int request_irq(unsigned int irq, <BR>void (*handler)(int irq, void *dev_id, struct pt_regs <BR>*regs), <BR>unsigned long irqflags, <BR>const char * devname, <BR>void *dev_id); <BR>如果共用中斷，irqflags設置SA_SHIRQ屬性，這樣就允許別的設備申請同一個 <BR>中斷。需要注意所有用到這個中斷的設備在調用request_irq()都必須設置這個屬 <BR>性。系統在回調每個中斷處理程式時，可以用dev_id這個參數找到相應的設備。一 <BR>般dev_id就設為device結構本身。系統處理共用中斷是用各自的dev_id參數依次調 <BR>用每一個中斷處理程式。 <BR><BR>3.2 硬體發送忙時的處理 <BR>主CPU的處理能力一般比網路發送要快，所以經常會遇到系統有資料要發，但 <BR>上一包資料網路設備還沒發送完。因為在Linux裏網路設備驅動程式一般不做資料 <BR>緩存，不能發送的資料都是通知系統發送不成功，所以必須要有一個機制在硬體不 <BR>忙時及時通知系統接著發送下面的資料。 <BR>一般對發送忙的處理在前面設備的發送方法(hard_start_xmit)裏已經描述過， <BR>即如果發送忙，置tbusy為1。處理完發送資料後，在發送結束中斷裏清tbusy，同 <BR>時用mark_bh()調用通知系統繼續發送。 <BR>但在具體實現我的驅動程式時發現，這樣的處理系統好象並不能及時地知道硬 <BR>件已經空閒了，即在mark_bh()以後，系統要等一段時間才會接著發送。造成發送 <BR>效率很低。2M線路只有10%不到的使用率。內核版本為2.0.35。 <BR>我最後的實現是不把tbusy置1，讓系統始終認為硬體空閒，但是報告發送不成 <BR>功。系統會一直嘗試重發。這樣處理就運行正常了。但是遍循內核源碼中的網路驅 <BR>動程式，似乎沒有這樣處理的。不知道癥結在哪里。 <BR><BR>3.3 流量控制(flow control) <BR>網路資料的發送和接收都需要流量控制。這些控制是在系統裏實現的，不需要 <BR>驅動程式做工作。每個設備資料結構裏都有一個參數dev-&gt;tx_queue_len，這個參數 <BR>標明發送時最多緩存的資料包。在Linux系統裏乙太網設備(10/100Mbps) <BR>tx_queue_len一般設置為100，串列線路(非同步串口)為10。實際上如果看源碼可以 <BR>知道，設置了dev-&gt;tx_queue_len並不是為緩存這些資料申請了空間。這個參數只是 <BR>在收到協定層的資料包時判斷發送佇列裏的資料是不是到了tx_queue_len的限度， <BR>以決定這一包資料加不加進發送佇列。發送時另一個方面的流控是更高層協議的發 <BR>送視窗(TCP協定裏就有發送視窗)。達到了視窗大小，高層協定就不會再發送資料。 <BR>接收流控也分兩個層次。netif_rx()緩存的資料包有限制。另外高層協定也會 <BR>有一個最大的等待處理的資料量。 <BR><BR>發送和接收流控處理在net/core/dev.c的do_dev_queue_xmit()和netif_rx() <BR>中。 <BR><BR>3.4 調試 <BR>很多Linux的驅動程式都是編譯進內核的，形成一個大的內核檔。但對調試 <BR>來說，這是相當麻煩的。調試驅動程式可以用module方式載入。支援模組方式的 <BR>驅動程式必須提供兩個函數：int init_module(void)和void <BR>cleanup_module(void)。 <BR>init_module()在載入此模組時調用，在這個函數裏可以register_netdev()註冊 <BR>設備。init_module()返回0表示成功，返回負表示失敗。cleanup_module()在驅動 <BR>程式被卸載時調用，清除佔用的資源，調用unregister_netdev()。 <BR>模組可以動態地載入、卸載。在2.0.xx版本裏，還有kerneld自動載入模組， <BR>但是2.2.xx中已經取消了kerneld。手工載入使用insmod命令，卸載用rmmod命令， <BR>看內核中的模組用lsmod命令。 <BR>編譯驅動程式用gcc，主要命令行參數-DKERNEL -DMODULE。並且作為模組載入 <BR>的驅動程式，只編譯成obj形式(加-c參數)。編譯好的目標檔放在/lib/modules <BR>/2.x.xx/misc下，在啟動檔裏用insmod載入。 <BR><BR><BR>四.進一步的閱讀 <BR>Linux程式設計資料可以從網上獲得。這就是開放源代碼的好處。並且沒有什 <BR>麼“未公開的秘密”。我編寫驅動程式時參閱的主要資料包括： <BR>Linux內核源代碼 <BR>&lt;<THE Linux Kernel Hackers Guide>&gt; by Michael K. Johnson <BR>&lt;<LINUX Kernel Guide Module Programming>&gt; by Ori Pomerantz <BR>&lt;<LINUX下的設備驅動程式>&gt; by olly in BBS水木清華站 <BR>可以選擇一個範本作為開始，內核源代碼裏有一個網路驅動程式的範本， <BR>drivers/net/skeleton.c。裏面包含了驅動程式的基本內容。但這個範本是以乙太 <BR>網設備為物件的，乙太網的處理在Linux系統裏有特殊“待遇”，所以如果不是以 <BR>太網設備，有些細節上要注意，主要在初始化程式裏。 <BR>最後，多參照別人寫的程式，聽聽其他開發者的經驗之談大概是最有效的幫助 <BR>了。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/487.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:31 <a href="http://www.cnitblog.com/drizztzou/articles/487.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Linux内核源代码的阅读和工具介绍</title><link>http://www.cnitblog.com/drizztzou/articles/484.html</link><dc:creator>【Z&amp;Y】幸福小筑</dc:creator><author>【Z&amp;Y】幸福小筑</author><pubDate>Thu, 23 Jun 2005 16:17:00 GMT</pubDate><guid>http://www.cnitblog.com/drizztzou/articles/484.html</guid><wfw:comment>http://www.cnitblog.com/drizztzou/comments/484.html</wfw:comment><comments>http://www.cnitblog.com/drizztzou/articles/484.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cnitblog.com/drizztzou/comments/commentRss/484.html</wfw:commentRss><trackback:ping>http://www.cnitblog.com/drizztzou/services/trackbacks/484.html</trackback:ping><description><![CDATA[随着linux的逐步普及，现在有不少人对于Linux的安装及设置已经比较熟悉了。与Linux 的蓬勃发展相适应，想深入了解Linux的也越来越多。而要想深入了解Linux，就需要阅读和分析linux内核的源代码。 <BR><BR>　　Linux的内核源代码可以从很多途径得到。一般来讲，在安装的linux系统下，/usr/src/linux目录下的东西就是内核源代码。另外还可以从互连网上下载,解压缩后文件一般也都位于linux目录下。内核源代码有很多版本，目前最新的稳定版是2.2.14。 <BR><BR>　　许多人对于阅读Linux内核有一种恐惧感，其实大可不必。当然，象Linux内核这样大而复杂的系统代码，阅读起来确实有很多困难，但是也不象想象的那么高不可攀。只要有恒心，困难都是可以克服的。也不用担心水平不够的问题，事实上，有很多事情我们不都是从不会到会，边干边学的吗？ <BR><BR>　　任何事情做起来都需要有方法和工具。正确的方法可以指导工作，良好的工具可以事半功倍。对于Linux 内核源代码的阅读也同样如此。下面我就把自己阅读内核源代码的一点经验介绍一下，最后介绍Window平台下的一种阅读工具。 <BR><BR>　　对于源代码的阅读，要想比较顺利，事先最好对源代码的知识背景有一定的了解。对于linux内核源代码来讲，我认为，基本要求是：1、操作系统的基本知识；2、对C语言比较熟悉，最好要有汇编语言的知识和GNU C对标准C的扩展的知识的了解。另外在阅读之前，还应该知道Linux内核源代码的整体分布情况。我们知道现代的操作系统一般由进程管理、内存管理、文件系统、驱动程序、网络等组成。看一下Linux内　　核源代码就可看出，各个目录大致对应了这些方面。Linux内核源代码的组成如下（假设相对于linux目录）： <BR><BR>　　arch 这个子目录包含了此核心源代码所支持的硬件体系结构相关的核心代码。如对于X86平台就是i386。 <BR><BR>　　include 这个目录包括了核心的大多数include文件。另外对于每种支持的体系结构分别有一个子目录。 <BR><BR>　　init 此目录包含核心启动代码。 <BR><BR>　　mm 此目录包含了所有的内存管理代码。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下，如对应于X86的就是arch/i386/mm/fault.c 。 <BR><BR>　　drivers 系统中所有的设备驱动都位于此目录中。它又进一步划分成几类设备驱动，每一种也有对应的子目录，如声卡的驱动对应于drivers/sound。 <BR><BR>　　ipc 此目录包含了核心的进程间通讯代码。 <BR><BR>　　modules 此目录包含已建好可动态加载的模块。 <BR><BR>　　fs Linux支持的文件系统代码。不同的文件系统有不同的子目录对应，如ext2文件系统对应的就是ext2子目录。 <BR><BR>　　kernel 主要核心代码。同时与处理器结构相关代码都放在arch/*/kernel目录下。 <BR><BR>　　net 核心的网络部分代码。里面的每个子目录对应于网络的一个方面。 <BR><BR>　　lib 此目录包含了核心的库代码。与处理器结构相关库代码被放在arch/*/lib/目录下。 <BR><BR>　　scripts此目录包含用于配置核心的脚本文件。 <BR><BR>　　Documentation 此目录是一些文档，起参考作用。 <BR><BR>　　清楚了源代码的结构组成后就可以着手阅读。对于阅读方法或者说顺序，有所谓的纵向与横向之分。所谓纵向就是顺着程序的执行顺序逐步进行；所谓横向，就是分模块进行。其实他们之间不是绝对的，而是经常结合在一起进行。对于Linux源代码来讲，启动的代码就可以顺着linux的启动顺序一步一步来，它的大致流程如下（以X86平台为例）： <BR><BR>　　./larch/i386/boot/bootSect.S--&gt;./larch/i386/boot/setup.S--&gt;./larch/i386/kernel/head.S--&gt;./init/main.c中的start_kernel()。而对于象内存管理等部分，则可以单独拿出来进行阅读分析。我的体会是：开始最好按顺序阅读启动代码，然后进行专题阅读，如进程部分，内存管理部分等。在每个功能函数内部应该一步步来。实际上这是一个反复的过程，不可能读一遍就理解。 <BR><img src ="http://www.cnitblog.com/drizztzou/aggbug/484.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cnitblog.com/drizztzou/" target="_blank">【Z&Y】幸福小筑</a> 2005-06-24 00:17 <a href="http://www.cnitblog.com/drizztzou/articles/484.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>