You are on page 1of 10

读 Cat 源码

I_amok
2009-04-13

上次读了 date 的源码,这下对 coreutils 的编码流程有了一些初步的了解了,这次看 cat 源码又花了


一周多,还是进展缓慢,我们来看看几个主要的东西,不会像 date 源码那样说的很详细了。

Cat 利用了一个 line_buf 的东西来保存具体的行号,如果你在 cat 中加 -n 参数输出行号的话。

这个数组就是用来保存行号的,实际上就是一个字串了,大小是 20,上面的说明有说,18 位数需要 1000


年,在一台 p2 的机器上。

然后就定义的行号的头尾的指针和具体输出行号的时候的打印指针,这个比较有趣,第一次看到这样的计
数器。

主要是说 从倒数的第 8 位开始为输出行号的长度,倒数第 3 都是行号的头尾数字的开始处。

这里为啥要-3 而不是-2 呢?因为我们看到第三位是 0,-3 不是把这个 0 都过去了么?因为数组都是 0 开始


计数的,并且长度是 20,代码里直接加 20,就已经超过 20 的长度了,变成了 21 个长度,所以要多减 1 才
能到达正确的位置,所以倒数第三的位置就要-3 而不是-2。这个比较绕,要仔细想想。
接着就是这个计数器的定义了,其实比较简单的,主要是把 line_num_end 赋值给一个函数的内部指针 endp,
这样每次 line_num_end 都会被重置(line_num_start 就不一样了,line_num_start 要记住他的位置,所
以不用赋值,直接用了),因为都要从最后一个数字遍历起(因为是计数器)。 如果尾数小于 9 就没事,就
跳出这个函数了。如果小于 9 就返回了,并且 endp 本身指向的数值被++了。如果等于 9,++后就跳过 return,
因为到 9 再++就变成 10 了,就要进位了,就把 endp 指向的数值变成 0,然后向前移动。(*endp-- = '0')
直到 endp >= line_num_start

然后接着把 line_num_start 左边移位后变成 1,同时判断是否超过数组的左边界,如果超过就变成>

最后判断起始的打印位置是否被 line_num_start 超过,如果超过,就向左移动其实的打印位置。


然后程序定义了一个叫做 simple_cat 的函数,用来在没有任何参数的 cat 的时候调用这个 simple_cat。

n_read = safe_read (input_desc, buf, bufsize );

safe_read 是系统自带函数 read 的一个外挂形式,在 lib/safe-read.c 有定义

/* Read(write) up to COUNT bytes at BUF from(to) descriptor FD, retrying if


interrupted. Return the actual number of bytes read(written), zero for EOF,
or SAFE_READ_ERROR(SAFE_WRITE_ERROR) upon error. */

就是说,通过指定 buf 大小,buf 指针,还有文件的 FD,从文件中读取 buf 大小的数据内容读取到 buf 指


针中,并且返回读取的具体读取的字节数

就是直接调用系统的 read,来读取文件里的内容到 buf 指针里。

size_ t n = n_read;
if (full_write (STDOUT_FILENO, buf, n) != n)

full_write 是定义在 lib/full-write.h

/* Write COUNT bytes at BUF to descriptor FD, retrying if interrupted


or if partial writes occur. Return the number of bytes successfully
written, setting errno if that is less than COUNT. */

因为 full_write 是定义成从 buf 中读取多少字节的东西写到其他地方,所以 n 就变成原先读取的大小。

其实就是调用 safe-write,而 safe-write 和 safe-read 是一个公用代码的定义,内容差不多,只是调用


的系统函数不同。
full-write 的主代码

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)
break;
if (n_rw == 0)
{
errno = ZERO_BYTE_TRANSFER_ERRNO;
break;
}
total += n_rw;
ptr += n_rw;
count -= n_rw;
}

return total;
}

因为 count 就是前面读取的字节数,所以 full-write 也要求写相应的大小,而且把 prt 的指针向后移动 n_rw


(注意,这个不会改动 full-write 的参数里面的 buf 数值,他改动的只是 prt 的数值,不明白他为啥这样
做,好像多此一举。后来仔细想想也不是多此一举,full-write 要保证给定的 count 要对应打印出这么多
的数据。),同时让 count-=n_rw 变成 0,从而跳出整个 while 循环.

整个 simple_cat 自定义函数的用意就是一边读,一边写。

最复杂的一个函数 cat 函数接着就被定义了,最主要是了解 里面的第一个 do....While 循环。


cat 函数很重要的一个东西就是 newlines 的定义

/* Determines how many consecutive newlines there have been in the


input. 0 newlines makes NEWLINES -1, 1 newline makes NEWLINES 1,
etc. Initially 0 to indicate that we are at the beginning of a
new line. The "state" of the procedure is determined by
NEWLINES. */

就是说如果 newlines 的值是-1,表示经过一个换行,


0 是初始化的状态,其他的数值都是连续几个真正的换行才会存在。。。

cat 函数几个判断的地方是

if (outbuf + outsize <= bpout) ,如果连续几个真正的换行,会导致 bpout 超过 outbuf+outsize 的所


以要提前先输出几个换行符号,或者如果 bpout 超过应该输出的 outsize 了,就强制输出了。

if (bpin > eob) ,如果到了用换行做标示的 buffer 的结尾部分,就执行新的读取任务

else,否则就是遇到真正的换行了,因为后面的一个 for 循环中,如果遇到换行,就返回 newlines 为-1,


表示遇到换行了,因为这个时候 bpin 还不会大于 eob,因为是在读取的内容中遇到的换行。
这个比较绕,要画图说明一下,看图:

这个是假设的 cat 读取的原始数据的结构。

当他读取到 inbuf 的时候就变成了:

因为只会读取 insize 大小的数据,所以中间如果有跨行读取,换行符会在中间,当读取到一个换行符的时


候,这个 bpin 并不会比 eob 大,所以 else 部分肯定就是一个真正的换行了。只有在 bpin>eob
的时候才是读取缓存末尾的换行。
这样就在两个 for 循环之间来回跳转,直到数据完全被读取和处理完毕。

注意后面的 while (ch == '\n'); 也就是说,只有当遇到回车的时候才会触发上面的这些动作,


1.输出到 STDOUT_FILENO,只有达到具体的输出大小了才会输出。
2.重新开始读取新的数据,只有达到 bpin > eob
3.直接输出换行符到 bpout,其他情况

为了理解

if (show_nonprinting)

后面的代码,好绕,我提取出来单独测试了一下

提取的代码如下:

#include <stdio.h>

int main (void)


{
char test_array[1000] ;

char * bpout = test_array ;


int ch = 128;

if (ch >= 32)


{
if (ch < 127)
*bpout++ = ch;
else if (ch == 127)
{
*bpout++ = '^';
*bpout++ = '?';
}
else
{
*bpout++ = 'M';
*bpout++ = '-';

printf ( "char = %c%c\n" , *(bpout-2), *(bpout-1) );


printf ( "one printf\n" );

if (ch >= 128 + 32)


{
if (ch < 128 + 127)
{
*bpout++ = ch - 128;
printf ( "char = %c\n" , *(bpout-1) );
printf ( "two printf\n");
}
else
{
*bpout++ = '^';
*bpout++ = '?';

printf ( "char = %c%c\n" , *(bpout-2), *(bpout-1) );


printf ( "four printf\n");
}
}
else
{
*bpout++ = '^';
*bpout++ = ch - 128 + 64;
printf ( "char = %c%c\n" , *(bpout-2), *(bpout-1) );
printf ( "five printf\n");
}
}
}

主要是没法理解最后一个 else 的部分是啥意思,通过测试原来是 ch >= 128 <160 的时候的情形。


并且只要 ch >= 128 都会打印 M-

也就是执行
*bpout++ = 'M';
*bpout++ = '-';
if (show_nonprinting)中只有遇到了换行符,才会跳出第二个 for 循环,然后进入到第一个 for 循环,进
行判断。

进入到主函数部分了

fstat (STDOUT_FILENO, &stat_buf)

取文件属性

/* Get file attributes for the file, device, pipe, or socket


that file descriptor FD is open on and put them in BUF. */
extern int fstat (int __fd, struct stat *__buf) __THROW __nonnull ((2));

/usr/include/sys/stat.h 定义。

------------------------------------------

ST_BLKSIZE: Preferred I/O blocksize for the file, in bytes.

src/system.h 定义

------------------------

S_ISREG (stat_buf.st_mode)

应该是用来判断文件是不是普通文件的。

----------------------

isatty

/* Return 1 if FD is a valid descriptor associated


with a terminal, zero if not. */
extern int isatty (int __fd) __THROW;

在 /usr/include/unistd.h 中定义
--------------------------
freopen (NULL, "wb", stdout);

/* Open a file, replacing an existing stream with it.

/usr/include/stdio.h 中定义

freopen
FILE *freopen(const char *restrict filename, const char *restrict mode,
FILE *stream);The function closes the file associated with the stream stream (as if by
calling fclose); then it opens the file with the filename filename and associates the file with
the stream stream (as if by calling fopen(filename, mode)). It returns stream if the open is
successful; otherwise, it returns a null pointer.

inbuf = xmalloc (insize + page_size - 1);

xmalloc 在 lib/xmalloc.c 有定义

/* Allocate N bytes of memory dynamically, with error checking. */

void *
xmalloc (size_ t n)
{
void *p = malloc (n);
if (!p && n != 0)
xalloc_die ();
return p;
}

ptr_align (inbuf, page_size)

是个 inline function

在 src/system.h 有定义
/* Return PTR, aligned upward to the next multiple of ALIGNMENT.
ALIGNMENT must be nonzero. The caller must arrange for ((char *)
PTR) through ((char *) PTR + ALIGNMENT - 1) to be addressable
locations. */

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);
}

ptr_align 的作用可能就是让 buffer 的 point 的起始点到结束位置的 size 正好能被 pagesize 除尽。

这个在 C 里有个术语叫做 alignment memory

Solve the memory alignment in C interview question that stumped me


http://stackoverflow.com/questions/227897/solve-the-memory-alignment-in-c-interview-
question-that-stumped-me
有介绍相关的说明

Memory alignment 这个帖子应该完整的解释了为啥要-1,其实是一个算法


http://groups.google.com/group/comp.lang.c.moderated/browse_thread/thread/a4b9b104a9ff967c?p
li=1

但是我有个不解的地方,他为啥不在分配内存的时候就对齐呢?还要去对齐指针,上面两个连接都是说在
内存分配的时候对齐的。

while (++argind < argc); 这个部分又用到了上面分析的 optind 的部分知识了,就是说当还有文件参数


的时候(因为选项参数已经处理完毕了,++argind 还小于 argc,就表示有多余的文件名参数)就一直执行
上面的过程,就是不断的 cat 新的文件。

这次分析写的比较粗,但是实际上我是看完一行行的代码的。这里只是说说我觉得比较难懂的几个部分,
这个帖子 https://bbs.be10.com/vbb3.0.1/showthread.php?t=3468
是我的读书笔记,大家可以跟踪。

You might also like