Linux cat 命令源码剖析

最近在读APUE, 边看还得边做才有效果. 正好linux下很多命令的是开源的, 可以直接看源码. GNU coreutils 是个不错的选择. 源码包有我们最常用的 ls, cat等命令的源码, 每个命令都比较短小精悍, 适合阅读. 下面是我阅读 cat 命令的一点笔记.

这里下载源码. 在源码根目录下 ./configure; make 就可以直接编译, 修改后make就可以编译了. 命令源码在 src/目录中, lib/目录下有一些用到的辅助函数和常量定义.


1. 命令行解析

基本上所有的Linux命令都是用getopt函数来解析命令行参数的, cat也不例外, cat使用的是getopt_long函数, 以便解析长参数, 用一些bool变量来存储选项值. 没什么好说的.


2. 检测输入输出文件是否相同

例如 cat test.txt > test.txt 的情况, 输入输出文件相同, 这是不合法的. 

cat 的输入流由命令行给定, 默认是标准输入(stdin), 输出流是标准输出(stdout). 所以用字符串比较的方法是无法判断输入输出是否是相同.  另外对于一些特殊的文件, 如tty, 我们是允许其输入输出相同的, 如 cat /dev/tty > /dev/tty 是合法的. cat采取的方式是对与regular file, 检测设备编号和i-node是否相同. 忽略对非regular file的检测. 这部分的代码如下:

获得文件属性.

if (fstat (STDOUT_FILENO, &stat_buf) < 0)
    error (EXIT_FAILURE, errno, _("standard output"));


提取文件设备编号和i-node. 对于非 regular 类型的文件, 忽视检测.

if (S_ISREG (stat_buf.st_mode))
    {
      out_dev = stat_buf.st_dev;
      out_ino = stat_buf.st_ino;
    }
  else
    {
      check_redirection = false;
    }

进行检查. check_redirection为false就不检查.

 if (fstat (input_desc, &stat_buf) < 0)<span style="white-space:pre">	</span>// input_desc为输入文件描述符
        {
          error (0, errno, "%s", infile);
          ok = false;
          goto contin;
        }

if (check_redirection
          && stat_buf.st_dev == out_dev && stat_buf.st_ino == out_ino
          && (input_desc != STDIN_FILENO))
        {
          error (0, 0, _("%s: input file is output file"), infile);
          ok = false;
          goto contin;
        }

Tips: ‘-‘  表示的是标准输入, 如 cat - 命令实际是从标准输入读取字节. 所以cat可以配合管道命令这样用: echo abcd | cat file1 - file2. 只输入 cat 命令默认就是从标准输入读取字节.

3. 一次读写的字节数目

cat是以read, write函数为基础实现的, 一次读写的字节数的多少也影响了程序的性能.

insize 和 outsize 变量分别表示一次读和写的字节数目.

insize = io_blksize (stat_buf);
enum { IO_BUFSIZE = 128*1024 };
static inline size_t
io_blksize (struct stat sb)
{
  return MAX (IO_BUFSIZE, ST_BLKSIZE (sb));<span style="white-space:pre">		</span>/* ST_BLKSIZE( )宏的值视系统而定, 在lib/stat-size.h中定义 */
}

outsize值的设定类似insize.


4. simple_cat

如 cat 命令不使用任何格式参数, 如 -v, -t. 那么就调用simple_cat来完成操作, simple_cat的优点是速度快, 因为它在某些系统上有可能是以二进制方式读写文件. 参考 man 3 freopen.

if (! (number || show_ends || squeeze_blank))
    {
      file_open_mode |= O_BINARY;<span style="white-space:pre">		</span>/* 在linux下O_BINARY为0, 没有任何效果, 但有些系统是表示二进制形式打开文件 */
      if (O_BINARY && ! isatty (STDOUT_FILENO))
<span style="white-space:pre">	</span>/* 调用 freopen, 包含错误处理, 将输出流的mode改为"wb" */
        xfreopen (NULL, "wb", stdout);
    }

无任何格式参数, 则调用simple_cat

 if (! (number || show_ends || show_nonprinting
             || show_tabs || squeeze_blank))
        {
          insize = MAX (insize, outsize);
<span style="white-space:pre">	</span> /* xzz 分配内存, 失败则调用 xmalloc-die() 终止程序并报告错误 */
          inbuf = xmalloc (insize + page_size - 1);

          ok &= simple_cat (<strong>ptr_align</strong> (inbuf, page_size), insize);
        }

ptr_align是一个辅助函数. 因为IO操作一次读取一页, ptr_align是使得缓冲数组的起始地址为也大小的整数倍, 以增加IO的效率.

static inline void *
ptr_align (void const *ptr, size_t alignment)
{
  char const *p0 = ptr;
  char const *p1 = p0 + alignment - 1;
  return (void *) (p1 - (size_t) p1 % alignment);
}


simple_cat函数很简单

static bool
simple_cat (
     /* Pointer to the buffer, used by reads and writes.  */
     char *buf,

     /* Number of characters preferably read or written by each read and write
        call.  */
     size_t bufsize)
{
  /* Actual number of characters read, and therefore written.  */
  size_t n_read;

  /* Loop until the end of the file.  */

  while (true)
    {
      /* Read a block of input.  */

	/*  普通的read可能被信号中断	*/
      n_read = safe_read (input_desc, buf, bufsize);
      if (n_read == SAFE_READ_ERROR)
        {
          error (0, errno, "%s", infile);
          return false;
        }

      /* End of this file?  */

      if (n_read == 0)
        return true;

      /* Write this block out.  */

      {
        /* The following is ok, since we know that 0 < n_read.  */
        size_t n = n_read;

		/* full_write 和 safe_read都调用的是 safe_sw, 用宏实现的,
		 * 查看 safe_write.c 就可以发现其实现的关键.
		 */
        if (full_write (STDOUT_FILENO, buf, n) != n)
          error (EXIT_FAILURE, errno, _("write error"));
      }
    }
}


5. safe_rw, full_rw 函数

read 和write函数在读写第一个字符之前有可能被signal中断, safe_rw可以恢复被中断的read和write过程. 这个函数非常tricky, 它的名字safe_rw和rw其实都是宏定义, 条件编译可以将此函数编译成safe_read, safe_write两个函数.

<strong>size_t </strong>/* 原始的read()函数返回值是 ssize_t */
safe_rw (int fd, void const *buf, size_t count)
{
  /* Work around a bug in Tru64 5.1.  Attempting to read more than
     INT_MAX bytes fails with errno == EINVAL.  See
     <http://lists.gnu.org/archive/html/bug-gnu-utils/2002-04/msg00010.html>.
     When decreasing COUNT, keep it block-aligned.  */
  enum { BUGGY_READ_MAXIMUM = INT_MAX & ~8191 };

  for (;;)
    {
      ssize_t result = rw (fd, buf, count);

      if (0 <= result)
        return result;
  <strong>    else if (IS_EINTR (errno))<span style="white-space:pre">	</span>/* signal 中断 */
        continue;</strong>
      else if (errno == EINVAL && BUGGY_READ_MAXIMUM < count)
        count = BUGGY_READ_MAXIMUM;
      else<span style="white-space:pre">		</span>/* 返回 (size_t) -1 */
        return result;
    }
}


read, write 读写过程中有可能被 signal 中断, full_rw 可以恢复读写过程, 直到读写到了指定数目的字节或到达文件结尾(EOF), 或者是读写出错. 返回当前已经读写了的字节数目. full_rw() 函数名也是宏定义的, 实际上实现了full_read() full_write().

/* Write(read) COUNT bytes at BUF to(from) descriptor FD, retrying if
   interrupted or if a partial write(read) occurs.  Return the number
   of bytes transferred.
   When writing, set errno if fewer than COUNT bytes are written.
   When reading, if fewer than COUNT bytes are read, you must examine
   errno to distinguish failure from EOF (errno == 0).  */
size_t
full_rw (int fd, const void *buf, size_t count)
{
  size_t total = 0;
  const char *ptr = (const char *) buf;

  while (count > 0)
    {
      size_t n_rw = safe_rw (fd, ptr, count);
      if (n_rw == (size_t) -1)<span style="white-space:pre">	</span>/* error */
        break;
      if (n_rw == 0)<span style="white-space:pre">	</span>/* reach EOF */
        {
          errno = ZERO_BYTE_TRANSFER_ERRNO;
          break;
        }
      total += n_rw;
      ptr += n_rw;
      count -= n_rw;
    }

  return total;
}

Tips : 查看lib目录下safe_read.c 和 safe_write.c文件可以看到这个函数怎样被展开成两个不同的函数的.


6. cat 函数, 处理格式化输出

simple_cat只是将输入原封不动的输出, 没有作任何处理, 所有与格式化输出有关的内容都放在了 cat 函数里面.

cat 函数的实现包含很多技巧. 例如使用一个哨兵‘\n‘来标记输入缓冲区的结尾. 另外使用了一个字符数组来统计行数, 使得不支持64位整型的系统也可以使用很大范围的数.

下面是这个行计数器的代码.

/* Position in 'line_buf' where printing starts.  This will not change
   unless the number of lines is larger than 999999.  */
static char *line_num_print = line_buf + LINE_COUNTER_BUF_LEN - 8;

/* Position of the first digit in 'line_buf'.  */
static char *line_num_start = line_buf + LINE_COUNTER_BUF_LEN - 3;

/* Position of the last digit in 'line_buf'.  */
static char *line_num_end = line_buf + LINE_COUNTER_BUF_LEN - 3;

static void
next_line_num (void)
{
  char *endp = line_num_end;
  do
    {
      if ((*endp)++ < '9')
        return;
      *endp-- = '0';
    }
  while (endp >= line_num_start);
  if (line_num_start > line_buf)
    *--line_num_start = '1';
  else
    *line_buf = '>';
  if (line_num_start < line_num_print)
    line_num_print--;
}

理解这个函数的关键是搞懂newlines这个变量作用, cat格式化输出主要的操作判断换行和连续空行, newlines这个变量标记的是空行的数目, 值为0表示此时的inbuf的读取位置在一行的开头, 1表示有一个空行, -1 表示刚刚解析完一行, 准备进入下一行, 可以看到cat 函数最后的那个 while(true) 的两个break语句都将 newlines 置为 -1.

cat格式化输出的过程实际上就是逐一扫描输入缓冲数组的过程, 并在扫描的过程中将转化后的字符存入输出缓冲数组中.


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。