图书节选:Linux 编程实例,第二部分

作者:Arnold Robbins

本文的第一部分中,我们开始了 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 NutshellLearning the vi EditorEffective awk Programming。他婚姻幸福,有四个可爱的孩子。Arnold 是一位业余塔木德学者,特别喜欢研究耶路撒冷塔木德。可以通过 arnold@skeeve.com 联系到他。

加载 Disqus 评论