图书摘录:Linux 编程示例,第一部分
本文摘自我撰写的书籍 Linux 编程示例:基础篇 的第 7 章,该书由 Prentice Hall 出版(ISBN:0-13-142964-7)。 前 1 至 6 章涵盖了以下标准 API:参数解析、动态内存管理、文件和目录访问、检索文件信息 (stat(),lstat())、用户和组操作、日期和时间信息以及排序。
V7 代码使用所谓的 K&R C,其中函数声明没有参数列表。对于此类声明,我们在右侧的下划线标记之间显示相应的 ANSI C 样式声明 [_收集所需信息_]。
V7 ls 命令很好地将我们目前所见的所有内容联系在一起。它使用了我们涵盖的几乎所有 API,涉及 Unix 编程的许多方面:内存分配、文件元数据、日期和时间、用户名、目录读取和排序。
1. V7 ls 选项
与现代版本的 ls 相比,V7 ls 仅接受少量选项,并且其中一些选项的含义对于 V7 而言与当前 ls 不同。 选项如下:
-a:打印所有目录条目。 没有此选项,则不打印 . 和 .. 有趣的是,V7 ls 仅忽略 . 和 ..,而 V1 到 V6 忽略任何名称以句点开头的文件。 后一种行为也是现代版本 ls 中的默认行为。
-c:将 inode 更改时间而不是修改时间与 -t 或 -l 一起使用。
-d:对于目录参数,打印有关目录本身的信息,而不是其内容。
-f:强制将每个参数读取为目录,并打印在每个槽中找到的名称。 此选项禁用 -l、-r、-s 和 -t,并启用 -a。(此选项显然用于文件系统调试和修复。)
-g:对于 ls -l,使用组名而不是用户名。
-i:在第一列中打印 inode 号以及文件名或长列表。
-l:提供熟悉的长格式输出。 但是请注意,V7 ls -l 仅打印用户名,而不是用户和组名。
-r:反转排序顺序,无论是文件名的字母顺序还是按时间排序。
-s:以 512 字节块为单位打印文件大小。 V7 ls (1) 手册页声明“间接块”(文件系统用于定位大型文件数据块的块)也包含在计算中,但是,正如我们将要看到的,此声明是不正确的。
-t:按修改时间(最近优先)而不是按名称对输出进行排序。
-u:将访问时间而不是修改时间与 -t 和/或 -l 一起使用。
V7 ls 和现代 ls 之间最大的区别在于 -a 选项和 -l 选项。 除非给出 -a,否则现代系统会省略所有点文件,并且它们在 -l 长列表中同时包含用户名和组名。 在现代系统上,-g 被理解为仅打印组名,而 -o 表示仅打印用户名。 值得一提的是,GNU ls 有 50 多个选项!
2. V7 ls 代码
V7 发行版中的文件 /usr/src/cmd/ls.c 包含代码。 它总共只有 425 行代码。
1 /* 2 * list file or directory 3 */ 4 5 #include <sys/param.h> 6 #include <sys/stat.h> 7 #include <sys/dir.h> 8 #include <stdio.h> 9 10 #define NFILES 1024 11 FILE *pwdf, *dirf; 12 char stdbuf[BUFSIZ]; 13 14 struct lbuf { _Collects needed info_ 15 union { 16 char lname[15]; 17 char *namep; 18 } ln; 19 char ltype; 20 short lnum; 21 short lflags; 22 short lnl; 23 short luid; 24 short lgid; 25 long lsize; 26 long lmtime; 27 }; 28 29 int aflg, dflg, lflg, sflg, tflg, uflg, iflg, fflg, gflg, cflg; 30 int rflg = 1; 31 long year; _Global variables: auto init to 0_ 32 int flags; 33 int lastuid = -1; 34 char tbuf[16]; 35 long tblocks; 36 int statreq; 37 struct lbuf *flist[NFILES]; 38 struct lbuf **lastp = flist; 39 struct lbuf **firstp = flist; 40 char *dotp = "."; 41 42 char *makename(); _char *makename(char *dir, char *file);_ 43 struct lbuf *gstat(); _struct lbuf *gstat(char *file, int argfl);_ 44 char *ctime(); _char *ctime(time_t *t);_ 45 long nblock(); _long nblock(long size);_ 46 47 #define ISARG 0100000
该程序以文件包含(第 5-8 行)和变量声明开始。 struct lbuf(第 14-27 行)封装了 struct stat 中 ls 感兴趣的部分。 我们稍后会看到如何填充此结构。
变量 aflg、dflg 等(第 29 和 30 行)都指示相应选项的存在。 这种变量命名风格是 V7 代码的典型风格。 flist、lastp 和 firstp 变量(第 37-39 行)表示 ls 报告信息的文件。 请注意,flist 是一个固定大小的数组,最多只能处理 1024 个文件。 我们很快就会看到所有这些变量是如何使用的。
变量声明之后是函数声明(第 42-45 行),然后是 ISARG 的定义,它区分了命令行上命名的文件和读取目录时找到的文件。
49 main(argc, argv) _int main(int argc, char **argv)_ 50 char *argv[]; 51 { 52 int i; 53 register struct lbuf *ep, **ep1; _Variable and function declarations_ 54 register struct lbuf **slastp; 55 struct lbuf **epp; 56 struct lbuf lb; 57 char *t; 58 int compar(); 59 60 setbuf(stdout, stdbuf); 61 time(&lb.lmtime); _Get current time_ 62 year = lb.lmtime - 6L*30L*24L*60L*60L; /* 6 months ago */
main() 函数首先声明变量和函数(第 52-58 行),设置标准输出的缓冲区,检索一天中的时间(第 60-61 行),并计算大约六个月前的 Epoch 以来的秒数值(第 62 行)。 请注意,所有常量都带有 L 后缀,表示使用长整型算术。
63 if (--argc > 0 && *argv[1] == '-') { 64 argv++; _Parse options_ 65 while (*++*argv) switch (**argv) { 66 67 case 'a': _All directory entries_ 68 aflg++; 69 continue; 70 71 case 's': _Size in blocks_ 72 sflg++; 73 statreq++; 74 continue; 75 76 case 'd': _Directory info, not contents_ 77 dflg++; 78 continue; 79 80 case 'g': _Group name instead of user name_ 81 gflg++; 82 continue; 83 84 case 'l': _Long listing_ 85 lflg++; 86 statreq++; 87 continue; 88 89 case 'r': _Reverse sort order_ 90 rflg = -1; 91 continue; 92 93 case 't': _Sort by time, not name_ 94 tflg++; 95 statreq++; 96 continue; 97 98 case 'u': _Access time, not modification time_ 99 uflg++; 100 continue; 101 102 case 'c': _Inode change time, not modification time_ 103 cflg++; 104 continue; 105 106 case 'i': _Include inode number_ 107 iflg++; 108 continue; 109 110 case 'f': _Force reading each arg as directory_ 111 fflg++; 112 continue; 113 114 default: _Ignore unknown option letters_ 115 continue; 116 } 117 argc--; 118 }
第 63-118 行解析命令行选项。 请注意手动解析代码 getopt() 尚未发明。 当选项需要使用 stat() 系统调用时,statreq 变量设置为 true。
避免在每个文件上进行不必要的 stat() 调用是一个很大的性能提升。 stat() 调用尤其昂贵,因为它可能涉及磁盘寻道到 inode 位置、磁盘读取以读取 inode,然后磁盘寻道回到目录内容的位置(以便继续读取目录条目)。
现代系统将 inode 分组,分散在整个文件系统中,而不是聚集在前端。 这使得性能得到了显着提高。 然而,stat() 调用仍然不是免费的;您应该根据需要使用它们,但不要过度使用。
119 if (fflg) { _-f overrides -l, -s, -t, adds -a_ 120 aflg++; 121 lflg = 0; 122 sflg = 0; 123 tflg = 0; 124 statreq = 0; 125 } 126 if(lflg) { _Open password or group file_ 127 t = "/etc/passwd"; 128 if(gflg) 129 t = "/etc/group"; 130 pwdf = fopen(t, "r"); 131 } 132 if (argc==0) { _Use current dir if no args_ 133 argc++; 134 argv = &dotp - 1; 135 }
第 119-125 行处理 -f 选项,关闭 -l、-s、-t 和 statreq。 第 126-131 行处理 -l,设置要读取用户或组信息的文件。 请记住,V7 ls 仅显示其中一个,而不是两者都显示。
如果没有剩余参数,则第 132-135 行设置 argv,使其指向表示当前目录的字符串。 赋值argv = &dotp - 1是有效的,尽管不常见。 - 1 补偿了第 137 行的 ++argv。 这避免了以下特殊情况代码:argc == 1在程序的主要部分中。
136 for (i=0; i < argc; i++) { _Get info about each file_ 137 if ((ep = gstat(*++argv, 1))==NULL) 138 continue; 139 ep->ln.namep = *argv; 140 ep->lflags |= ISARG; 141 } 142 qsort(firstp, lastp - firstp, sizeof *lastp, compar); 143 slastp = lastp; 144 for (epp=firstp; epp<slastp; epp++) { _Main code, see text_ 145 ep = *epp; 146 if (ep->ltype=='d' && dflg==0 || fflg) { 147 if (argc>1) 148 printf("\n%s:\n", ep->ln.namep); 149 lastp = slastp; 150 readdir(ep->ln.namep); 151 if (fflg==0) 152 qsort(slastp,lastp - slastp,sizeof *lastp,compar); 153 if (lflg || sflg) 154 printf("total %D\n", tblocks); 155 for (ep1=slastp; ep1<lastp; ep1++) 156 pentry(*ep1); 157 } else 158 pentry(ep); 159 } 160 exit(0); 161 } _End of main()_
第 136-141 行循环遍历参数,收集有关每个参数的信息。 gstat() 的第二个参数是一个布尔值:如果名称是命令行参数,则为 true,否则为 false。 第 140 行将 ISARG 标志添加到每个命令行参数的 lflags 字段。
gstat() 函数将每个新的 struct lbuf 添加到全局 flist 数组中(第 137 行)。 它还会更新 lastp 全局指针,使其指向此数组中当前的最后一个元素。
第 142-143 行使用 qsort() 对数组进行排序,并将 lastp 的当前值保存在 slastp 中。 第 144-159 行循环遍历数组中的每个元素,根据需要打印文件或目录信息。
目录的代码值得进一步解释
if (ep->ltype=='d' && dflg==0 || fflg) ...第 146 行。 如果文件类型是目录,并且如果未提供 -d 或者 如果提供了 -f,则 ls 必须读取目录,而不是打印有关目录本身的信息。
if (argc>1) printf("\n%s:\n", ep->ln.namep)第 147-148 行。 如果在命令行上命名了多个文件,则打印目录名称和一个冒号。
lastp = slastp; readdir(ep->ln.namep)第 149-150 行。 从 slastp 重置 lastp。 flist 数组充当文件名的两级堆栈。 命令行参数保存在 firstp 到 slastp - 1 中。 当 readdir() 读取目录时,它将目录内容的 struct lbuf 结构放在堆栈上,从 slastp 开始并一直到 lastp。 这在图 7.1 中进行了说明。
firstp slastp lastp | | | V V V +--------+--------+--------+--------+--------+--------+ | struct | struct | struct | struct | struct | struct | flist array | lbuf * | lbuf * | lbuf * | lbuf * | lbuf * | lbuf * | +--------+--------+--------+--------+--------+--------+ |<--- From command line -->|<---- From readdir() ---->|
图 7.1:作为两级堆栈的 flist 数组
if (fflg==0) qsort(slastp,lastp - slastp,sizeof *lastp,compar)第 151-152 行。 如果 -f 未生效,则对子目录条目进行排序。
if (lflg || sflg) printf("total %D\n", tblocks)第 153-154 行。 对于 -l 或 -s,打印目录中文件使用的块总数。 此总数保存在变量 tblocks 中,该变量为每个目录重置。 printf() 的 %D 格式字符串等效于现代系统上的 %ld;它表示打印长整数。(V7 也有 %ld,请参见第 192 行。)
for (ep1=slastp; ep1>lastp; ep1++) pentry(*ep1)第 155-156 行。 打印有关子目录中每个文件的信息。 请注意,V7 ls 仅在目录树中下降一级。 它缺少现代的 -R 递归选项。
163 pentry(ap) _void pentry(struct lbuf *ap)_ 164 struct lbuf *ap; 165 { 166 struct { char dminor, dmajor;}; _Unused historical artifact_ 167 register t; _from V6 ls_ 168 register struct lbuf *p; 169 register char *cp; 170 171 p = ap; 172 if (p->lnum == -1) 173 return; 174 if (iflg) 175 printf("%5u ", p->lnum); _Inode number_ 176 if (sflg) 177 printf("%4D ", nblock(p->lsize)); _Size in blocks_
pentry() 例程打印有关文件的信息。 第 172-173 行检查 lnum 字段是否为 1,如果是,则返回。 当p->lnum == -1为 true 时,struct lbuf 无效。 否则,此字段是文件的 inode 号。
如果 -i 生效,则第 174-175 行打印 inode 号。 如果 -s 生效,则第 176-177 行打印块总数。(正如我们在下面看到的,此数字可能不准确。)
178 if (lflg) { _Long listing:_ 179 putchar(p->ltype); _-- File type_ 180 pmode(p->lflags); _-- Permissions_ 181 printf("%2d ", p->lnl); _-- Link count_ 182 t = p->luid; 183 if(gflg) 184 t = p->lgid; 185 if (getname(t, tbuf)==0) 186 printf("%-6.6s", tbuf); _-- User or group_ 187 else 188 printf("%-6d", t); 189 if (p->ltype=='b' || p->ltype=='c') _-- Device: major/minor_ 190 printf("%3d,%3d", major((int)p->lsize), minor((int)p->lsize)); 191 else 192 printf("%7ld", p->lsize); _-- Size in bytes_ 193 cp = ctime(&p->lmtime); 194 if(p->lmtime < year) _-- Modification time_ 195 printf(" %-7.7s %-4.4s ", cp+4, cp+20); else 196 printf(" %-12.12s ", cp+4); 197 } 198 if (p->lflags&ISARG) _-- Filename_ 199 printf("%s\n", p->ln.namep); 200 else 201 printf("%.14s\n", p->ln.lname); 202 }
第 178-197 行处理 -l 选项。 第 179-181 行打印文件类型、权限和链接数。 第 182-184 行根据 -g 选项将 t 设置为用户 ID 或组 ID。 第 185-188 行检索相应的名称,并在可用时打印它。 否则,程序将打印数值。
第 189-192 行检查文件是否为块设备或字符设备。 如果是,则它们使用 major() 和 minor() 宏提取并打印主设备号和次设备号。 否则,它们将打印文件大小。
第 193-196 行打印感兴趣的时间。 如果时间早于六个月,则代码将打印月份、日期和年份。 否则,它将打印月份、日期和时间(*注意 Ctime::,有关 ctime() 结果的格式)。
最后,第 198-201 行打印文件名。 对于命令行参数,我们知道它是一个以零结尾的字符串,可以使用 %s。 对于从目录读取的文件,它可能不是以零结尾的,因此必须使用显式精度 %.14s。
204 getname(uid, buf) _int getname(int uid, char buf[])_ 205 int uid; 206 char buf[]; 207 { 208 int j, c, n, i; 209 210 if (uid==lastuid) _Simple caching, see text_ 211 return(0); 212 if(pwdf == NULL) _Safety check_ 213 return(-1); 214 rewind(pwdf); _Start at front of file_ 215 lastuid = -1; 216 do { 217 i = 0; _Index in buf array_ 218 j = 0; _Counts fields in line_ 219 n = 0; _Converts numeric value_ 220 while((c=fgetc(pwdf)) != '\n') { _Read lines_ 221 if (c==EOF) 222 return(-1); 223 if (c==':') { _Count fields_ 224 j++; 225 c = '0'; 226 } 227 if (j==0) _First field is name_ 228 buf[i++] = c; 229 if (j==2) _Third field is numeric ID_ 230 n = n*10 + c - '0'; 231 } 232 } while (n != uid); _Keep searching until ID found_ 233 buf[i++] = '\0'; 234 lastuid = uid; 235 return(0); 236 }
getname() 函数将用户或组 ID 号转换为相应的名称。 它实现了一个简单的缓存方案;如果传入的 uid 与全局变量 lastuid 相同,则该函数返回 0,表示 OK;缓冲区将已包含名称(第 210-211 行)。 lastuid 初始化为 -1(第 33 行),因此此测试在第一次调用 getname() 时失败。
pwdf 已在 /etc/passwd 或 /etc/group 上打开(请参见第 126-130 行)。 此处的代码检查打开是否成功,如果失败,则返回 -1(第 212-213 行)。
令人惊讶的是,ls 不 使用 getpwuid() 或 getgrgid()。 相反,它利用了 /etc/passwd 和 /etc/group 的前三个字段(名称、密码、数字 ID)的格式相同,并且都使用冒号作为分隔符的事实。
第 216-232 行实现了对文件的线性搜索。 j 计算到目前为止看到的冒号数:名称为 0,ID 号为 2。 因此,在扫描该行时,它会同时填充名称和 ID 号。
第 233-235 行终止名称缓冲区,将全局 lastuid 设置为找到的 ID 号,并返回 0 表示 OK。
在本文的后半部分,我们将完成对 V7 ls 代码文件的巡视。
Arnold Robbins 是一名专业的程序员和技术作家。 他是 gawk(GNU 项目的 Awk 编程语言版本)的长期维护者。 他是许多著名的技术书籍的作者,例如最近由 Prentice Hall 出版的 Linux 编程示例:基础篇,以及 O'Reilly Media Inc. 出版的 Unix In A Nutshell、Learning the vi Editor 和 Effective awk Programming。 他婚姻幸福,育有四个可爱的孩子。 Arnold 是一位业余的塔木德学者,特别喜欢研究耶路撒冷塔木德。 您可以通过 arnold@skeeve.com 与他联系。