图书节选:Linux 编程实例,第二部分
在本文的第一部分中,我们开始了 V7 Unix ls 程序的巡览。我们研究了它是如何解析选项的,以及它是如何将 flist 数组维护为要打印信息的文件的两级堆栈的。然后我们看到了 ls 如何查找并打印出关于每个文件的长格式信息。本文继续巡览代码。
238 long _long nblock(long size)_ 239 nblock(size) 240 long size; 241 { 242 return((size+511)>>9); 243 }
nblock() 函数报告文件使用了多少磁盘块。此计算基于 stat() 返回的文件大小。V7 块大小为 512 字节——物理磁盘扇区的大小。
第 242 行的计算看起来有点吓人。>>9 是右移九位。这除以 512,得到块数。(在早期的硬件上,右移比除法快得多。)到目前为止,一切都很好。现在,即使只有一个字节的文件仍然占用一个完整的磁盘块。但是,1 / 512 的结果为零(整数除法会截断),这是不正确的。这就解释了 size+511。通过加 511,代码确保总和在除以 512 时产生正确的块数。
然而,此计算只是近似值。非常大的文件也有间接块。尽管 V7 _ls_(1) 手册页中声明,但此计算并未考虑间接块。
此外,考虑具有大空洞的文件的情况(通过使用 lseek() 定位到文件末尾之后创建)。空洞不占用磁盘块;但是,这并未反映在大小值中。因此,nblock() 产生的计算虽然通常是正确的,但可能会产生小于或大于实际情况的结果。
由于这些原因,st_blocks 成员被添加到 4.2 BSD 的 struct stat 中,然后被 System V 和 POSIX 采用。
245 int m1[] = { 1, S_IREAD>>0, 'r', '-' }; 246 int m2[] = { 1, S_IWRITE>>0, 'w', '-' }; 247 int m3[] = { 2, S_ISUID, 's', S_IEXEC>>0, 'x', '-' }; 248 int m4[] = { 1, S_IREAD>>3, 'r', '-' }; 249 int m5[] = { 1, S_IWRITE>>3, 'w', '-' }; 250 int m6[] = { 2, S_ISGID, 's', S_IEXEC>>3, 'x', '-' }; 251 int m7[] = { 1, S_IREAD>>6, 'r', '-' }; 252 int m8[] = { 1, S_IWRITE>>6, 'w', '-' }; 253 int m9[] = { 2, S_ISVTX, 't', S_IEXEC>>6, 'x', '-' }; 254 255 int *m[] = { m1, m2, m3, m4, m5, m6, m7, m8, m9}; 256 257 pmode(aflag) void pmode(int aflag) 258 { 259 register int **mp; 260 261 flags = aflag; 262 for (mp = &m[0]; mp < &m[sizeof(m)/sizeof(m[0])];) 263 select(*mp++); 264 } 265 266 select(pairp) void select(register int *pairp) 267 register int *pairp; 268 { 269 register int n; 270 271 n = *pairp++; 272 while (--n>=0 && (flags&*pairp++)==0) 273 pairp++; 274 putchar(*pairp); 275 }
第 245-275 行打印文件的权限。代码紧凑且相当优雅;需要仔细研究。
第 245-253 行:数组 m1 到 m9 编码要检查的权限位以及要打印的相应字符。要打印的文件模式中的每个字符都有一个数组。每个数组的第一个元素是该特定数组中编码的(权限,字符)对的数量。最后一个元素是在未找到任何给定权限位时要打印的字符。
另请注意权限是如何指定为 I_READ<<0、I_READ>>3、I_READ<<6 等的。每个位的单个常量(S_IRUSR、S_IRGRP 等)尚未发明。(请参阅我的书中的表 4.5。)
第 255 行:m 数组指向 m1 到 m9 数组中的每一个。
第 257-264 行:pmode() 函数首先将全局变量 flags 设置为传入的参数 aflag。然后它循环遍历 m 数组,将每个元素传递给 select() 函数。传入的元素表示 m1 到 m9 数组之一。
第 266-275 行:select() 函数理解每个 m1 到 m9 数组的布局。n 是数组中对的数量(第一个元素);第 271 行设置它。第 272-273 行查找权限位,检查先前在第 261 行设置的全局变量 flags。
注意 ++ 运算符的用法,无论是在循环测试中还是在循环体中。效果是在数组中跳过对,只要在对的第一个元素中未找到权限位 flags。
当循环结束时,或者找到了权限位,在这种情况下,pairp 指向对的第二个元素,即要打印的正确字符,或者未找到权限位,在这种情况下,pairp 指向默认字符。在任何一种情况下,第 274 行都会打印 pairp 指向的字符。
最后值得注意的一点是,在 C 语言中,字符常量(例如 x)的类型为 int,而不是 char。这在 C++ 中是不同的:在那里,字符常量确实具有 char 类型。这种差异不会影响此特定代码。因此,将此类常量放入整数数组中没有问题;一切都正常工作。
277 char * _char *makename(char *dir, char *file)_ 278 makename(dir, file) 279 char *dir, *file; 280 { 281 static char dfile[100]; 282 register char *dp, *fp; 283 register int i; 284 285 dp = dfile; 286 fp = dir; 287 while (*fp) 288 *dp++ = *fp++; 289 *dp++ = '/'; 290 fp = file; 291 for (i=0; i<DIRSIZ; i++) 292 *dp++ = *fp++; 293 *dp = 0; 294 return(dfile); 295 }
第 277-295 行定义了 makename() 函数。它的工作是连接目录名和文件名,用斜杠字符分隔,并生成一个字符串。它在静态缓冲区 dfile 中执行此操作。请注意,dfile 只有 100 个字符长,并且未进行错误检查。
代码本身很简单,一次复制一个字符。readdir() 函数使用 makename()。
297 readdir(dir) _void readdir(char*dir)_ 298 char *dir; 299 { 300 static struct direct dentry; 301 register int j; 302 register struct lbuf *ep; 303 304 if ((dirf = fopen(dir, "r")) == NULL) { 305 printf("%s unreadable\n", dir); 306 return; 307 } 308 tblocks = 0; 309 for(;;) { 310 if (fread((char *)&dentry, sizeof(dentry), 1, dirf) != 1) 311 break; 312 if (dentry.d_ino==0 313 || aflg==0 && dentry.d_name[0]=='.' && (dentry.d_name[1]=='\0' 314 || dentry.d_name[1]=='.' && dentry.d_name[2]=='\0')) 315 continue; 316 ep = gstat(makename(dir, dentry.d_name), 0); 317 if (ep==NULL) 318 continue; 319 if (ep->lnum != -1) 320 ep->lnum = dentry.d_ino; 321 for (j=0; j<DIRSIZ; j++) 322 ep->ln.lname[j] = dentry.d_name[j]; 323 } 324 fclose(dirf); 325 }
第 297-325 行定义了 readdir() 函数,其工作是读取命令行上命名的目录的内容。
第 304-307 行打开目录以进行读取,如果 fopen() 失败则返回。第 308 行将全局变量 tblocks 初始化为 0。这在前面(第 153-154 行)用于打印目录中文件使用的总块数。
第 309-323 行是一个循环,它读取目录条目并将它们添加到 flist 数组中。第 310-311 行读取一个条目,在文件末尾退出循环。
第 312-315 行跳过不感兴趣的条目。如果 inode 号为零,则未使用此槽。否则,如果未给出 -a 并且文件名是 . 或 ..,则跳过它。
第 316-318 行使用文件的完整名称调用 gstat(),第二个参数为 false,表示它不是来自命令行。gstat() 更新全局 lastp 指针和 flist 数组。NULL 返回值表示某种类型的故障。
第 319-322 行将 inode 号和名称保存在 struct lbuf 中。如果 ep->lnum 从 gstat() 返回并设置为 -1,则表示对文件的 stat() 操作失败。最后,第 324 行关闭目录。
以下函数 gstat()(第 327-398 行)是检索和存储文件信息的关键函数。
327 struct lbuf * _struct lbuf *gstat(char *file, int argfl)_ 328 gstat(file, argfl) 329 char *file; 330 { 331 extern char *malloc(); 332 struct stat statb; 333 register struct lbuf *rep; 334 static int nomocore; 335 336 if (nomocore) _Ran out of memory earlier_ 337 return(NULL); 338 rep = (struct lbuf *)malloc(sizeof(struct lbuf)); 339 if (rep==NULL) { 340 fprintf(stderr, "ls: out of memory\n"); 341 nomocore = 1; 342 return(NULL); 343 } 344 if (lastp >= &flist[NFILES]) { 345 static int msg; _Check whether too many files given_ 346 lastp--; 347 if (msg==0) { 348 fprintf(stderr, "ls: too many files\n"); 349 msg++; 350 } 351 } 352 *lastp++ = rep; _Fill in information_ 353 rep->lflags = 0; 354 rep->lnum = 0; 355 rep->ltype = '-'; _Default file type_
静态变量 nomocore [原文如此] 表示 malloc() 在先前的调用中失败。由于它是静态的,因此会自动初始化为 0(即 false)。如果在入口处为 true,则 gstat() 只需返回 NULL。否则,如果 malloc() 失败,ls 会打印一条错误消息,将 nomocore 设置为 true,并返回 NULL(第 334-343 行)。
第 344-351 行确保 flist 数组中仍有空间。如果没有,ls 会打印一条消息(但仅打印一次;请注意静态变量 msg 的使用),然后重用 flist 中的最后一个槽。
第 352 行使 lastp 指向的槽指向新的 struct lbuf (rep)。这也更新了 lastp,它用于在 main() 中排序(第 142 行和第 152 行)。第 353-355 行设置 struct lbuf 中标志、inode 号和类型字段的默认值。
356 if (argfl || statreq) { 357 if (stat(file, &statb)<0) { _stat() failed_ 358 printf("%s not found\n", file); 359 statb.st_ino = -1; 360 statb.st_size = 0; 361 statb.st_mode = 0; 362 if (argfl) { 363 lastp--; 364 return(0); 365 } 366 } 367 rep->lnum = statb.st_ino; _stat() OK, copy info_ 368 rep->lsize = statb.st_size; 369 switch(statb.st_mode&S_IFMT) { 370 371 case S_IFDIR: 372 rep->ltype = 'd'; 373 break; 374 375 case S_IFBLK: 376 rep->ltype = 'b'; 377 rep->lsize = statb.st_rdev; 378 break; 379 380 case S_IFCHR: 381 rep->ltype = 'c'; 382 rep->lsize = statb.st_rdev; 383 break; 384 } 385 rep->lflags = statb.st_mode & ~S_IFMT; 386 rep->luid = statb.st_uid; 387 rep->lgid = statb.st_gid; 388 rep->lnl = statb.st_nlink; 389 if(uflg) 390 rep->lmtime = statb.st_atime; 391 else if (cflg) 392 rep->lmtime = statb.st_ctime; 393 else 394 rep->lmtime = statb.st_mtime; 395 tblocks += nblock(statb.st_size); 396 } 397 return(rep); 398 }
第 356-396 行处理对 stat() 的调用。如果这是一个命令行参数,或者由于选项 statreq 为 true,则代码按如下方式填充 struct lbuf
第 357-366 行:调用 stat(),如果失败,则打印一条错误消息并设置适当的值,然后返回 NULL(表示为 0)。
第 367-368 行:如果 stat() 成功,则从 struct stat 设置 inode 号和大小字段。
第 369-384 行:处理目录、块设备和字符设备的特殊情况。在所有情况下,代码都会更新 ltype 字段。对于设备,lsize 值将替换为 ltype 字段。对于设备,lsize 值将替换为 st_rdev 值。
第 385-388 行:从 struct stat 中的相应字段填充 lflags、luid、lgid 和 lnl 字段。第 385 行删除文件类型位,留下 12 个权限位(用户/组/其他的读/写/执行,以及 setuid、setgid 和 save-text)。
第 389-394 行:根据命令行选项,使用 struct stat 中的三个时间字段之一作为 struct lbuf 中的 lmtime 字段。
第 395 行:使用文件中的块数更新全局变量 tblocks。
400 compar(pp1, pp2) _int compar(struct lbuf_ **pp1, 401 struct lbuf **pp1, **pp2; _struct lbuf_ **pp2) 402 { 403 register struct lbuf *p1, *p2; 404 405 p1 = *pp1; 406 p2 = *pp2; 407 if (dflg==0) { 408 if (p1->lflags&ISARG && p1->ltype=='d') { 409 if (!(p2->lflags&ISARG && p2->ltype=='d')) 410 return(1); 411 } else { 412 if (p2->lflags&ISARG && p2->ltype=='d') 413 return(-1); 414 } 415 } 416 if (tflg) { 417 if(p2->lmtime == p1->lmtime) 418 return(0); 419 if(p2->lmtime > p1->lmtime) 420 return(rflg); 421 return(-rflg); 422 } 423 return(rflg * strcmp(p1->lflags&ISARG? p1->ln.namep: p1->ln.lname, 424 p2->lflags&ISARG? p2->ln.namep: p2->ln.lname)); 425 }
compar() 函数很密集:在很小的空间里发生了很多事情。首先要记住的是返回值的含义:负值表示第一个文件应在数组中排序到早于第二个文件的位置,零表示文件相等,正值表示第二个文件应在数组中排序到早于第一个文件的位置。
接下来要理解的是,ls 在打印关于文件的信息之后打印目录的内容。因此,排序的结果应该是所有在命令行上命名的目录都跟随所有在命令行上命名的文件。
最后,rflg 变量有助于实现 -r 选项,该选项反转排序顺序。它初始化为 1(第 30 行)。如果使用 -r,则 rflg 设置为 -1(第 89-91 行)。
以下伪代码描述了 compar() 的逻辑;左边距中的行号与 ls.c 的行号相对应
407 if LS HAS TO READ DIRECTORIES # dflg == 0 408 if P1 IS A COMMAND-LINE ARG AND P1 IS A DIRECTORY 409 if P2 IS NOT A COMMAND-LINE ARG AND IS NOT A DIRECTORY 410 return 1 # first comes after second else FALL THROUGH TO TIME TEST 411 else # p1 is not a command-line directory 412 if P2 IS A COMMAND-LINE ARG AND IS A DIRECTORY 413 return -1 # first comes before second else FALL THROUGH TO TIME TEST 416 if SORTING IS BASED ON TIME # tflg is true # compare times: 417 if P2'S TIME IS EQUAL TO P1'S TIME 418 return 0 419 if P2'S TIME > P1'S TIME 420 RETURN THE VALUE OF RFLG (POSITIVE OR NEGATIVE) # p2's time < p1's time 421 RETURN OPPOSITE OF RFLG (NEGATIVE OR POSITIVE) 423 Multiply rflg by the result of strcmp() 424 on the two names and return the result
第 423-424 行中 strcmp() 的参数看起来很混乱。发生的事情是,必须使用 struct lbuf 中 ln 联合的不同成员,具体取决于文件名是命令行参数还是从目录读取的。
V7 ls 是一个相对较小的程序,但它触及了 Unix 编程的许多基本方面:文件 I/O、文件元数据、目录内容、用户和组、时间和日期值、排序和动态内存管理。
V7 ls 和现代 ls 之间最显着的外部差异是对 -a 和 -l 选项的处理。V7 版本比现代版本选项少得多;一个明显的缺点是缺少 -R 递归选项。
flist 的管理是一种干净的方式,可以在 PDP-11 架构的有限内存中使用,但仍然提供尽可能多的信息。struct lbuf 很好地从 struct stat 中抽象出感兴趣的信息;这大大简化了代码。打印九个权限位的代码紧凑而优雅。
ls 的某些部分使用了令人惊讶的小限制,例如文件数量的上限 1024 或 makename() 中的缓冲区大小 100。
Arnold Robbins 是一位专业的程序员和技术作家。他是 gawk 的长期维护者,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 联系到他。