You are on page 1of 21

Csplit 源码研读

I_amok
2009.04.28

这个是我开始读的第三个源码,也是我到现在感觉最难的一个,用了 2 个星期, 汗....

static void
keep_new_line (struct buffer_record *b, char *line_start, size_ t line_len)

先来看看 keep_new_line 的流程.

也就是说,如果一次性读取 80 个字节(CTRL_SIZE 的数值),这 80 字节都是 newline,而前面又遗留下了


一些 newline,这样就需要多申请一个 struct line 了(也就是组成了 struct line 这个数据结构链表),
不过这个假设又不成立,因为后面的 load_buffer 函数,已经确保了不会有 line 遗留在 hold_area 的情况发
生,真是多此一举.

然后每个 buffer_record->curr_line->starts 这个数组里面都是存储着对应 buffer_record->buffer


内存地址的指针和对应的 line 的长度.

record_line_starts 解读。

line_end = memchr (line_start, '\n', bytes_left);

memchr 的用法如下:

void *memchr(const void *s, int c, size_ t n);

The function searches for the first element of an array of unsigned ch ar, beginning at the
address s with size n, that equals (unsigned char)c. If successful, it returns the address of
the matching element; otherwise, it returns a null pointer.

就是说从数组的 s 的第一个字符开始到第 n 个长度为止查找 char c,如果找到就返回这个 c 的地址,否则


就是空指针。

套用的语句里就是从 line_start 开始到第 byte_left 字节,查找 newline 符号。

lines = 0;
line_start = b->buffer;
bytes_left = b->bytes_used;

for (;;)
{
line_end = memchr (line_start, '\n', bytes_left);
if (line_end == NULL)
break;
line_length = line_end - line_start + 1;
keep_new_line (b, line_start, line_length);
bytes_left -= line_length;
line_start = line_end + 1;
lines++;
}
这段代码主要是说在 b->buffer 里搜索 b->bytes_used 长度的数据,看看是否有 newline 符号,如果有,
就调用 keep_new_line 这个数据结构来保存 line_start(指针)和 line_length,并且 lines++

如果没有 newline 就跳出 for 循环。

xmemdup 在 ./lib/xmalloc.c 中定义。

/* Clone an object P of size S, with error checking. There's no need


for xnmemdup (P, N, S), since xmemdup (P, N * S) works without any
need for an arithmetic overflow check. */

void *
xmemdup (void const *p, size_ t s)
{
return memcpy (xmalloc (s), p, s);
}

xmemdup 其实是调用了 memcpy

来看看 memcpy 的说明。

memcpy
void *memcpy(void *restrict s1, const void *restrict s2, size_ t n);The function copies the
array of char beginning at the address s2 to the array of char beginning at the address s1
(both of size n). It returns s1. The elements of the arrays can be accessed and stored in
any order.

xmemdup 的作用就是返回一个地址,内容就是 line_start 开始到 bytes_left 的数据。

这里之所以要用 xmemdup 是因为后面 load_buffer 如果没有找到 newline 的时候会 free 掉 buffer_record,


xmemdup 就会重新分配一块内存空间,然后返回这个新地址,后面即使 free 掉 buffer_record 了,也能有
指针指向真实的内容。

然后 save_to_hold_area 分别把这两个东西,一个是地址,一个是长度,保存在全局变量
hold_area 和 hold_count 中
然后更新 buffer_record 里面的 num_lines,start_line,first_available
last_line_number,并且返回 lines。

static void
save_buffer (struct buffer_record *buf)
{
struct buffer_record *p;

buf->next = NULL;
buf->curr_line = buf->line_start;

if (head == NULL)
head = buf;
else
{
for (p = head; p->next; p = p->next)
/* Do nothing. */ ;
p->next = buf;
}
}

这个代码其实就是跟踪 buffer_record 的链表,然后把 next 为 null 的更新为 buffer 的地址。

跟踪的代码就靠这个:

for (p = head; p->next; p = p->next)


/* Do nothing. */ ;

然后把 p->next 赋值为 buf 地址。

在搞清楚了 buffer_record 和 struct line 之间的关系后,save_buffer 的动作也能理解了,


在 save 动作里,就是把 buf->line_start 赋值给 buf->curr_line,便于下次使用时能直接定位到当前行
就是开始。
如果 head 这个全局变量是 NULL 那么表示这个是开头,就不用做什么遍历 buffer_record 的操作了,反之,
就用一个空的 for 循环,判断 buffer_record->next 是否为空,不空就把 next 赋值给当前的 buffer_record,
然后把 buf 附加到 buffer_record->next 上

static bool
load_buffer (void)

开始 load 数据了

if (bytes_wanted < hold_count)


bytes_wanted = hold_count;

先对比前面遗留的数据大小和 bytes_wanted 的大小,那个大用那个。

b = get_new_buffer (bytes_wanted);
bytes_avail = b->bytes_alloc; /* Size of buffer returned. */
p = b->buffer;

接着就调用 get_new_buffer 来生成新的 buffer 了

同时从新分配的 buffer_record 里提取 b->bytes_alloc 和 b->buffer

/* First check the `holding' area for a partial line. */


if (hold_count)
{
memcpy (p, hold_area, hold_count);
p += hold_count;
b->bytes_used += hold_count;
bytes_avail -= hold_count;
hold_count = 0;
}

如果前面有数据就读取数据到新生成的 buffer_record 里
用的是 memcpy 的方式,copy hold_count 的数据,然后移动 b->buffer 的指针到+hold_count
然后更新 b->bytes_used,减少 bytes_avail,hold_count=0

然后通过

b->bytes_used += read_input (p, bytes_avail);

利用自定义函数从 stdin 里面读取可以数据,大小就是还剩下的没有使用的空间。

这里可能有点疑问,如果 hold_count 传输到 get_new_buffer 里作为分配内存的大小,这里还会有


bytes_avail 剩下么?答案是有,因为 get_new_buffer 里有做这样的判断。

alloc_size = START_SIZE ;
if (alloc_size < min_size )
{
size_ t s = min_size - alloc_size + INCR_SIZE - 1;
alloc_size += s - s % INCR_SIZE ;
}

保证了,当剩下的 hold_count 大于默认的 alloc_size 的时候,所做的设置。


暂时先不管他什么意思,反正知道可以有 bytes_avail 给 read_input 使用就是了。

然后通过 lines_found = record_line_starts (b); 查找一个完整的行

if (!lines_found)
free_buffer (b);

这个代码说,如果没有找到一个 newline 就清空这次申请的缓存,因为不够大。

xalloc_oversized 在 ./lib/xalloc.h 中有定义

xalloc_oversized 这个宏主要用来判断 2 * b->bytes_alloc 不会超过 系统的限制。

然后接着指定分配内存大小为 2* b->bytes_alloc

然后清空前面分配的太小内存地址
free_buffer (b); 清空 b->buffer
free (b); 清空 buffer_record

然后继续这个 while 循环

整个 load_buffer 的作用就是一定要找到一行,然后保存到 buffer_record 这个链表里,


否则就加大分配的内存空间,读取的也就多了,目的就是要读取到一个完整的行。

返回真,如果找到一行,返回假,找到 EOF

static struct cstring *


remove_line (void)

主要作用是从 line 这个数据结构里取出最前面的 line

static struct buffer_record *prev_buf = NULL;

remove_line 中定义了一个函数的局部静态变量,用来记录已经读取过的 buffer_record

如果有 prev_buf 就 free 他

同时如果 head 为 null,就调用 load_buffer()

用了一个 if (++l->retrieve_index == l->used)

把 struct line 移动到下一个 struct line 链表,判断

if (head->curr_line == NULL || head->curr_line->used == 0)

成立,也就是 struct line 里面没有 next 了,或者说分配了根本就没有用.

就把 prev_buf 赋值成 head,同时 head-next 赋值给 head.

同时返回获取的 line 的地址指针.


if (current_line < head->first_available)
current_line = head->first_available;

++(head->first_available);

这段代码是用来让 fist_available 转换成 current_line 的...用来更新 current_line 这个全局变量.

l = head->curr_line; 这里注意下 ,因为 keep_new_line 里面是 head->curr_line 指向下一个 line


struct 了 ,但是在 save_buffer 里面 ,有这样的一 句 . buf->curr_line = buf->line_start; 又把
curr_line 变成 buf->line_start 了.:) 所以这里这样用是直接指向到 buffer_record 里的第一个 line
struce.

static struct cstring *


find_line (uintmax_t linenum)

在 buffer 里查找输入的行号的指针

for (b = head;;)
{
if (linenum < b->start_line + b->num_lines)

这个代码主要是判断输入的行号,是否在 buffer 所包含的范围内,

start_line 是目前个 buffer_record 的开始行号(也就是算总数的)


num_lines 是目前这个 buffer_record 的具体行数

主要判断 linenum 是不是在当前这个 buffer_record 里


offset = linenum - b->start_line;
/* Find the control record. */
while (offset >= CTRL_SIZE )
{
l = l->next;
offset -= CTRL_SIZE ;
}

这个主要是判断当前这个 line 的数据结构中的行数是否在 linenum 的范围内,如果不够,就要进入 line


数据链表的下一个分支。

最后返回具体的行指针。

如果没有找到符合
if (linenum < b->start_line + b->num_lines)

条件的,就进入下一个 buffer_record 块
b = b->next;

static void
process_line_count (const struct control *p, uintmax_t repetition)

主要是通过 uintmax_t last_line_to_save = p->lines_required * (repetition + 1);

获取到 last_line_to_save 然后 create_output_file ();

建立一个输出文件,然后用 while 循环
while (linenum++ < last_line_to_save)

save_line_to_file (line);

关闭文件
close_output_file ();
/* Ensure that the line number specified is not 1 greater than
the number of lines in the file. */
if (no_more_lines ())
handle_line_error (p, repetition);

最后一步不知道是干啥的.

uintmax_t last_line_to_save = p->lines_required * (repetition + 1);

这个语句中 repetition + 1 是因为,外部调用 process_line_count 的时候,repetition 参数的取值是


从 0 开始的.....

static void
process_regexp (struct control *p, uintmax_t repetition)

中的 re_search 在 ./lib/regexec.c 中定义 re_search 实际上是调用 re_search_stub

文件头在./lib/regex.h

/* Search in the string STRING (with length LENGTH) for the pattern
compiled into BUFFER. Start searching at position START, for RANGE
characters. Return the starting position of the match, -1 for no
match, or -2 for an internal error. Also return register
information in REGS (if REGS and BUFFER->no_sub are nonzero). */

re_search_stub 在 lib/regexec.c 中定义

这个函数通过传入 control *p 和 repetition (重复次数)

对 cstring * line 进行 regexp 判断..


查找匹配或者不匹配的,代码里用了一个 if 判断 offset 是否为正或者为负.

为正的时候,就是要往后追加写入的行数(offset)

为负的时候就是要往前退 offset,就不需要追加写,通过 write_to_file 一起写.

/* Return a new, initialized control record. */

static struct control *


new_control_record (void)

#define X2NREALLOC(P, PN) ((void) verify_true (sizeof *(P) != 1), \


x2nrealloc (P, PN, sizeof *(P)))

X2NREALLOC 在./src/system.h 中定义


这里 define 里面用逗号的用法是,在前面用一个 Compile Time Asserts 的方式,利用 verify_true 进行判
断,如果 sizeof *(p)!=1 就在编译的时候报错了 ,如果不报错,就返回后面 x2nrealloc 函数的值. 最终
x2nrealloc 还是调用的 xrealloc

x2nrealloc 在 ./lib/xalloc.h 中有定义

static_inline void *
x2nrealloc (void *p, size_ t *pn, size_ t s)
{
size_ t n = *pn;

if (! p)
{
if (! n)
{
/* The approximate size to use for initial small allocation
requests, when the invoking code specifies an old size of
ze ro. 64 bytes is the largest "small" request for the
GNU C library malloc. */
enum { DEFAULT_MXFAST = 64 };

n = DEFAULT_MXFAST / s;
n += !n;
}
}
else
{
/* Set N = ceil (1.5 * N) so that progress is made if N == 1.
Check for overflow, so that N * S stays in size_ t range.
The check is slightly conservative, but an exact check isn't
worth the trouble. */
if ((size_t) -1 / 3 * 2 / s <= n)
xalloc_die ();
n += (n + 1) / 2;
}

*pn = n;
return xrealloc (p, n * s);
}

/* Reallocate memory without fail. This works like xmalloc. Note,


realloc type functions are not suitable for attribute malloc since
they may return the same address across multiple calls. */

extern PTR xrealloc PARAMS ((PTR, size_ t));

xrealloc 在 /usr/include/libiberty.h 有定义

PARAMS 的定义

PARAMS ((prototype))
-- for functions which take a fixed number of arguments. Use this
when declaring the function. When defining the function, write a
K+R style argument list. For example:

char *strcpy PARAMS ((char *dest, char *source));


...
char *
strcpy (dest, source)
char *dest;
char *source;
{ ... }

在 /usr/include/ansidecl.h 中有说明

同样的头文件里有这样的 define

#define PARAMS(ARGS) ARGS

x2nrealloc 中的 *pn = n; 更新了 pn 所指的数值,这样,传址进来的 pn 就被更新了.

我还 没 理 解 这 个 x2nrealloc 具体 的 原 理 , 我暂 时 理 解 为 , 他通 过 判 断 controls 的大 小 , 还有
control_allocated 的值来决定是否要增加 controls 的内存分配.... ,每次增加的值为 n += (n + 1)
/ 2; 只有当
control_used == control_allocated 才进行新的分配.这个算法具体有啥意义我还没理解.

原来 ./lib/xalloc.h 注释部分我没有认证看,都有说明

/* If P is null, allocate a block of at least *PN such objects;


otherwise, reallocate P so that it contains more than *PN objects
each of S bytes. *PN must be nonzero unless P is null, and S must
be nonzero. Set *PN to the new number of objects, and return the
pointer to the new block. *PN is never set to ze ro, and the
returned pointer is never null.

Repeated reallocations are guaranteed to make progress, either by


allocating an initial block with a nonzero size, or by allocating a
larger block.

In the following implementation, nonzero sizes are increased by a


factor of approximately 1.5 so that repeated reallocations have
O(N) overall cost rather than O(N**2) cost, but the
specification for this function does not guarantee that rate.
Here is an example of use:

因为 csplit 代码里,control_allocated 初始化为 0,而且最开始 controls 这个数据结构指针(实际上是一


个数组)是 null,所以匹配代码里的

if (! p)
{
if (! n)

所以就是

enum { DEFAULT_MXFAST = 64 };

n = DEFAULT_MXFAST / s;
n += !n;

来分配 n 的大小了. 当第一次用过以后,就是用 else 下面的那个部分代码了.


同时初始化 controls 里面的元素.

/* Check if there is a numeric offset after a regular expression.


STR is the entire command line argument.
P is the control record for this regular expression.
NUM is the numeric part of STR. */

static void
check_for_offset (struct control *p, const char *str, const char *num)

根据注释是判断命令行参数里,regexp 后面是否跟着 数字.


/* Given that the first character of command line arg STR is '{',
make sure that the rest of the string is a valid repeat count
and store its value in P.
ARGNUM is the ARGV index of STR. */

static void
parse_repeat_count (int argnum, struct control *p, char *str)

通过给定一个{ 开头的 命令行参数,判断{}里面的是否为* 或者数字,*表示无限制的重复

if (xstrtoumax (str + 1, NULL, 10, &val, "") != LONGINT_OK)

这个语句是把{}中的字符形式数字转换成数字类型,并把地址传给&val

可能是 xstrtoumax 需要 str 是个完整的字符窜,所以前面有个 *end = '\0'; 动作,在最终代码结束的时


候有 *end = '}'; 这个是技巧的地方,要记一下.

并且传入给 p->repeat = val;

xstrtoumax 在 lib/xstrtoumax.c 中定义,最终还是在 lib/xstrtol.c 中定义的

/* Extract the regular expression from STR and check for a numeric offset.
STR should start with the regexp delimiter character.
Return a new control record for the regular expression.
ARGNUM is the ARGV index of STR.
Unless IGNORE is true, mark these lines for output. */

static struct control *


extract_regexp (int argnum, bool ignore, char const *str)
closing_delim = strrchr (str + 1, delim);

这句话是说 ,str 是开始边界 ,从开始边界起查找最后一个和开始边界相同的指针位置. strrchr 把 null


char 也看做 str 的一部分.

len = closing_delim - str - 1;

是说求两个定界符之间的长度,所以要多减 1,假设 1 -3 ,3-1=2 ,而中间只有一个 2,长度是 1.

err = re_compile_pattern (str + 1, len, &p->re_compiled);

re_compile_pattern 应该是一个系统函数 ./lib/regex.h 中有定义,

/* Compile the regular expression PATTERN, with length LENGTH


and syntax given by the global `re_syntax_options', into the buffer
BUFFER. Return NULL if successful, and an error string if not. */
extern const char *re_compile_pattern (const char *__pattern, size_ t __length,
struct re_pattern_buffer *__buffer);

就是给定一个字串,长度,输出的 buffer,然后输出编译过的 regexp 表达式.

最后判断一下 regexp 定界符后面是否跟着数字,

if (closing_delim[1])
check_for_offset (p, str, closing_delim + 1);

因为 closing_delim[0]是定界符本身, [1]就是后面紧跟的数字了.

并调用 check_for_offset 设置 p->offset

/* Extract the break patterns from args START through ARGC - 1 of ARGV.
After each pattern, check if the next argument is a repeat count. */

static void
parse_patterns (int argc, int start, char **argv)
整个函数是用来分析命令行里的 pattern 的.

首相他用了一个判断 if (*argv[i] == '/' || *argv[i] == '%')

看看是不是有 regexp 表达式,有就用 extract_regexp 来生成新的 control 数据结构指针

没有就用 new_control_record 生成一个

同时把这些非 regexp 的 patterns 用 xstrtoumax 转换成整型的,赋值给&val

同时,把 val 赋值给 last_val

if (i + 1 < argc && *argv[i + 1] == '{')

同时判断参数里是否有{ 有的就调用 parse_repeat_count 进行 p->repeat 的赋值.

这里命令行里肯定有个 FILE 的参数,所以会有 i+1 <argc ,因为你肯定要指定文件的.

更新 :

如果 opt 参数被处理完毕后,第一个可用的参数不是/或者%开头的,那就肯定是指定的行数了.

所以,把 p->argnum = i; i 赋值给 p->argnum 了.

同时把 i 转换成数字形式的 if (xstrtoumax (argv[i], NULL, 10, &val, "") != LONGINT_OK)

然后把它同函数里的 static 变量 last_val 做对比.

不能等于 last_val 也不能小于 last_val,也不能等于 0

最后再赋值给 p->lines_required

static unsigned int


get_format_flags (char **format_ptr)
static size_ t
get_format_width (char **format_ptr)

static size_ t
get_format_prec (char **format_ptr)

static void
get_format_conv_type (char **format_ptr)

get_format_conv_type 中有这样一句话,我开始很纳闷,不知道干嘛的

unsigned char ch = *(*format_ptr)++;

做了一个测试,原来是把 format_ptr 这个字串指针的第一个字串的第一个字符赋值给 ch,然后把第一个字


串的起始位置向后移动.

测试代码如下:

#include <stdio.h>

int main (void)


{
char ch;
char * array[] = {
"happy",
"birthday",
"to you"
};
//第一个指针的具体数值
printf ( "%p\n" , *array );

//打印出第一个指针所指的具体数值
printf ( "%c\n", **array );

//第二个指针的具体数值
printf ("%p\n",*array + 1);

//打印出第二个指针所指的字符
printf ( "%c\n", *(*array+1) );
//做偏移指针的动作

ch=*(*array)++;

printf ("after point ++ \n\n");

//看看偏移后的指针和具体数值
printf ( "%p\n" , *array );
printf ( "%c\n", **array );

printf ("the char is %c\n" , ch );

return 0;

输出如下:

0x8048538
h
0x8048539
a
after point ++

0x8048539
a
the char is h

可以看到做偏移动作后,字串的第一个位置就后移了.

static size_t
max_out (char *format)

原来 max_out 是在后面的这里使用呢
if (suffix)
filename_space = xmalloc (strlen (prefix) + max_out (suffix) + 2);

就是用来处理后缀文件名的.

max_out 和里面所调用的子函数都是传址形式的. 也就是说里面的一切偏移操作都会影响到 format 的指针


位置.

就是一步步的移动指针往后判断 suffix 的内容,

只有 [字符]%width.precision[diouxX]

这样的 suffix 才合适. 咳,这几个代码好绕呀.

max_out 就是计算这个格式所需要的最小长度.

set_input_file (argv[optind++]);

parse_patterns (argc, optind, argv);

因为前面的 getopt_long 会在操作完成后把 optind 向后移动 1 个位置(data 代码里有说明),所以这里的


set_input_file 只要用 argv[optind++] 而不是 argv[++optind] ,这个要注意一下,因为 csplit 的
man 里要求输入文件在 pattern 前面.
然后 parse_patterns 就直接用 optind 了,因为已经++过了

最后发一个 split_file 的流程图


草稿可以看 https://bbs.be10.com/vbb3.0.1/showthread.php?t=3477&page=1&pp=10

感慨一下,这个程序用了很多的自定义函数调用,麻烦死了,一看标题,原来是 1 人写,1 人改的,难怪了,看的


怪累的....

You might also like