图书摘录:Linux 编程示例,第一部分

作者:Arnold Robbins

本文摘自我撰写的书籍 Linux 编程示例:基础篇 的第 7 章,该书由 Prentice Hall 出版(ISBN:0-13-142964-7)。 前 1 至 6 章涵盖了以下标准 API:参数解析、动态内存管理、文件和目录访问、检索文件信息 (stat(),lstat())、用户和组操作、日期和时间信息以及排序。

V7 代码使用所谓的 K&R C,其中函数声明没有参数列表。对于此类声明,我们在右侧的下划线标记之间显示相应的 ANSI C 样式声明 [_收集所需信息_]。

整合所有内容:ls

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

加载 Disqus 评论