注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

从文件描述符缓存看脚本语言(awk)  

2013-12-28 20:21:47|  分类: 脚本语言 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、问题
比方说,对于一个文件,我们系统按照某种特征将不同的文件内容写入到不同的文件中,分别写入的文件数量又比较多,假设说有1W个可能的文件。使用一种脚本语言的时候我们系统尽量减少文件的打开关闭操作,也就是文件打开之后就不再关闭,使用的时候直接写入文件中,此时写入效率最高。
通常在bash中这些操作可能是通过下面的伪代码实现
for record in records
do
printf record >> file$((recofd.ID%10000))
done
虽然这里只是一个printf指令,但是bash依然是通过修改输出描述符,之后再还原的方式来进行重定向。这也就是说,如果record的数量为1KW,那么这个 "保存->重定向-->>打印-->>还原"的步骤将执行1KW次,系统的大部分哦开销都消耗在了这个低效的地方。
二、bash中保持文件描述符打开
1、bash执行的主要操作
在bash中,可以通过
exec  12 >> file
来将将指定的文件描述符12追加到file对应的文件中,这个看似简单的操作同样必须经过内核的帮助。对于上面的例子,假设file是一个文件名(不需要进行变量展开),
shell需要执行的系统调用只要有两个,第一步,通过open系统调用打开file文件,第二步,通过dup2系统调用将打开的文件描述符复制(duplicate)到目标文件描述符12中。
如果file是一个数字,或者通过变量展开之后为一个数字,此时会减少open这一步,但是dup2这一步操作不可避免。更为糟糕的是,如果一个命令(无论是独立命令还是shell的内置命令函数),执行结束后shell还需要执行相应的回滚操作,即把原来的文件描述符还原。
2、bash重定向实现
bash的重定向workhorse为do_redirection_internal函数,其主要功能就是对于各种重定向请求进行大的switc case实现
  switch (ri)
    {
    case r_output_direction:
    case r_appending_to:
      if (flags & RX_ACTIVE)
{
 if (flags & RX_UNDOABLE)
   {
     /* Only setup to undo it if the thing to undo is active. */
     if ((fd != redirector) && (fcntl (redirector, F_GETFD, 0) != -1))
add_undo_redirect (redirector, ri, -1);此处添加了回滚重定向列表,
     else
add_undo_close_redirect (redirector);
   }
……
 if ((fd != redirector) && (dup2 (fd, redirector) < 0))
   return (errno);
}
注意到这个dup2操作在大部分情况下都是定打不饶的一个操作,这个地方无法优化。
当函数执行完毕之后,这个undo_list将会被执行已还原之前重定向操作,逻辑在execute_command_internal函数中实现
 if (do_redirections (command->redirects, RX_ACTIVE|RX_UNDOABLE) != 0)
    {
……
      return (last_command_exit_value = EXECUTION_FAILURE);
    }

  if (redirection_undo_list)
    {
      my_undo_list = (REDIRECT *)copy_redirects (redirection_undo_list);
      dispose_redirects (redirection_undo_list);
      redirection_undo_list = (REDIRECT *)NULL;
    }
  else
    my_undo_list = (REDIRECT *)NULL;
 switch (command->type)
{
Do command
}
  if (my_undo_list)
    {
      do_redirections (my_undo_list, RX_ACTIVE);
      dispose_redirects (my_undo_list);
    }
3、内核对dup2的实现
这里可以看到,内核中并没有要求dup2中的目的文件描述符一定存在,一个进程打开的文件描述符可以不连续。内核中对于一个进程的文件描述符扩展实现主要位于
static struct fdtable * alloc_fdtable(unsigned int nr)
{
struct fdtable *fdt;
char *data;

/*
* Figure out how many fds we actually want to support in this fdtable.
* Allocation steps are keyed to the size of the fdarray, since it
* grows far faster than any of the other dynamic data. We try to fit
* the fdarray into comfortable page-tuned chunks: starting at 1024B
* and growing in powers of two from there on.
*/
nr /= (1024 / sizeof(struct file *));
nr = roundup_pow_of_two(nr + 1);
nr *= (1024 / sizeof(struct file *)); 这里代码大致的意思作者在注释中也说明了,希望能够尽量满足页面对齐,基于一个进程的文件描述符变化及增长会比其它动态结构更快。每次增长都是2的整数幂,以便于页面对齐。
if (nr > NR_OPEN)
nr = NR_OPEN;

fdt = kmalloc(sizeof(struct fdtable), GFP_KERNEL);通过kmalloc申请,而没有通过页面系统申请
……
}
4、bash对于文件描述符重定向的解析实现
文件的重定向和一个命令参数不太容易区分清楚。对于bash的重定向,文件描述符(数字)和重定向符号之间不能有空格,这个对于bash这种空白无关的语法分析器解析起来并不容易。bash中的语法文件为parse.y
redirection: '>' WORD
{
 redir.filename = $2;
 $$ = make_redirection (1, r_output_direction, redir);
}
| '<' WORD
{
 redir.filename = $2;
 $$ = make_redirection (0, r_input_direction, redir);
}
| NUMBER '>' WORD
{
 redir.filename = $3;
 $$ = make_redirection ($1, r_output_direction, redir);
}
可以看到,NUMBER和'>'并没有连在一起,而是作为单独的token来处理,那么NUMBER和'>'之间是否有空格是由谁在何处实现的呢?bash如何区分
echo 5>file 和 echo 5 > file
这一点就在bash的词法分析其中实现,它对于这种情况进行了特殊处理,这可能也是bash没有使用通用的词法分析工具而是自己定义实现的原因吧:在read_token_word函数中:
  /* Check to see what thing we should return.  If the last_read_token
     is a `<', or a `&', or the character which ended this token is
     a '>' or '<', then, and ONLY then, is this input token a NUMBER.
     Otherwise, it is just a word, and should be returned as such. */
  if MBTEST(all_digit_token && (character == '<' || character == '>' || \
   last_read_token == LESS_AND || \
   last_read_token == GREATER_AND))
      {
if (legal_number (token, &lvalue) && (int)lvalue == lvalue)
 yylval.number = lvalue;
else
 yylval.number = -1;
return (NUMBER);
      }
作者同样有一个注释(可见注释的意义:对于大部分人可能存在疑惑的地方添加注释):当读取到的一个token全部是数字的时候,此时词法分析器会peek下一个字面单词,如果它是重定向副('>' 或.<.),此时返回NUMBER词法单位,否则返回的词法单位是WORD,从而帮助语法分析器区别这两种情况。这也解释了一个现象,bash的所有重定向中,重定向符的左侧必须为数字(而不能为变量展开之后内容)
tsecer@harry #cat massopen.sh
#!/bin/sh
#for ((i = 0; i < 1000; i++))
{
exec $((i+1000))>file$((i+1000))
}
tsecer@harry #sh -x massopen.sh
+ exec 1000
massopen.sh: line 4: 1000: not found
tsecer@harry #
三、使用awk
1、文件描述符缓冲
记得之前使用awk管道处理一个文本时遇到过问题,当时也看了下awk的实现,awk明确说明了自己的文件描述符是必须主动关闭的,否则该文件描述符会一直存在。
在awk看来,管道文件和普通文件相同,例如
printf xxx > sort
printf xxx | sort
的区别并不大,它们都对应一个文件描述符,都对应一个字符串,awk对它们的操作相同,只是前者通过fopen打开,而后者通过popen打开。当awk缓存了普通磁盘文件的时候,管道文件同样不会被关闭,并且和普通文件一样,这个唯一的管道也是通过管道另一端的字符串唯一确定。
这样做的好处在于对已同一个管道文件,awk可以通过多次print或者getline调用来每次只取出一个record,便于将该操作放在一个循环中,例如
tsecer@harry #cat multigetline 
#!/bin/sh
END {
while ("ls /proc" | getline lst)
{
print lst
}
}
2、一些现象
tsecer@harry #cat batchsort.awk 
#!/bin/awk
END {
for (i = 0; i < 3 ; i++)
{
  for (j = 0; j <= i; j++)
        {
                print j | "sort -n"
        }
        #close("sort -n")
}
}
tsecer@harry #awk -f batchsort.awk /dev/null 
0
0
0
1
1
2
可以看到,最终排序的结果是按照所有打印结束之后统一排序打印。如果在外层循环中关闭文件描述符,此时排序将会为我们期望结果:
tsecer@harry #cat batchsort.awk 
#!/bin/awk
END {
for (i = 0; i < 3 ; i++)
{
  for (j = 0; j <= i; j++)
        {
                print j | "sort -n"
        }
        close("sort -n")
}
}
tsecer@harry #awk -f batchsort.awk /dev/null 
0
0
1
0
1
2
3、awk对于重定向的实现
redirect(NODE *redir_exp, int redirtype, int *errflg)
for (rp = red_head; rp != NULL; rp = rp->next) {重定向时会先从当前所有重定向中搜索可复用项。
/* now check for a match */
if (strlen(rp->value) == redir_exp->stlen
   && memcmp(rp->value, str, redir_exp->stlen) == 0
   && ((rp->flag & ~(RED_NOBUF|RED_EOF|RED_PTY)) == tflag
|| (outflag != 0
   && (rp->flag & (RED_FILE|RED_WRITE)) == outflag))) {

int rpflag = (rp->flag & ~(RED_NOBUF|RED_EOF|RED_PTY));
int newflag = (tflag & ~(RED_NOBUF|RED_EOF|RED_PTY));

if (do_lint && rpflag != newflag)
lintwarn(
_("unnecessary mixing of `>' and `>>' for file `%.*s'"),
(int) redir_exp->stlen, rp->value);

break;
}
}
在do_getline_redir函数中,如果管道已经关闭,此时并不关闭管道
if (cnt == EOF) {
/*
* Don't do iop_close() here if we are
* reading from a pipe; otherwise
* gawk_pclose will not be called.
*/
if ((rp->flag & (RED_PIPE|RED_TWOWAY)) == 0) {
(void) iop_close(iop);
rp->iop = NULL;
}
rp->flag |= RED_EOF; /* sticky EOF */
return make_number((AWKNUM) 0.0);
}
4、awk中为什么 pattern和action必须写在同一行
在awk的语法文件中,
END 
{
}
这样的写法是不符合语法规定的,虽然有时候这样看起来更加的简洁(至少在我看来),看起来更新一个函数,或者是yacc的语法结构。没有这样定义的原因在于这种写法存在二义性:
根据awk的语法,pattern和action均可以为空,当pattern为空是,表示匹配所有的record;当action为空时,执行默认的print动作,所以这个连在一起的写法是不合法的。
但是对去其它的语言,例如tcl(expect工具使用的基础语言),它要求一个if或者while的左括号必须和关键字在同一样并不是有二义性,而是为了简化词法分析,事实上,整个tcl语言并没有语法文件,这也是tcl语言当初设计的一个初衷,就是尽量不加任何限制,每行一个命令。在进行词法分析时,如果遇到一个左括号,就一直读取,直到遇到结束的右括号为止,这样的词法分析最为简单。
这种传统可能的确影响到了之后的编程风格,至少在linux内核和大部分开源C程序中,左括号一般都是和关键字在同一样,不过linux解释内核中这种编码风格的原因是为了减少翻屏。
  评论这张
 
阅读(410)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017