趁热打铁
前边讨论sed -i 与sed的差别时提到:ed实际上是全文拷贝到内存做镜象,通过编辑镜象和回写保存整个文件。
sed是流编辑器,对流有感性理解的人不难理解sed -i的临时文件的作用:做为流的输入
sed -i可以形象的比喻成:把一桶水原来的标签拿掉,换个新标签,通过“管道”将水倒进一个新的桶里
那么ed呢,ed是处理方式则是:把桶里的水“倒(实际上是复制)”到另一个桶里,直接对桶进行处理,再倒回原来的桶。
见man ed原文
If invoked with a file argument, then a copy of file is read into the editor’s buffer. Changes are made to this copy and not directly to file itself.
cu上有版主提示说,ed是有临时文件的,但在man里并没有仔细的提及,细翻源码发现,确定有运行tmpfile()程序
tmpfile程序是stdio里包含的一个标准函数,通过创建一个临时文件返回描述符,
且在较的版本里,ed在创建tmpfile后,直接unlink 该文件
[root@rac0 ed-1.5]# strace ./ed execve("./ed", ["./ed"], [/* 31 vars */]) = 0 ... open("/tmp/tmpfyDwG6P", O_RDWR|O_CREAT|O_EXCL, 0600) = 3 unlink("/tmp/tmpfyDwG6P") = 0
还可以通过lsof查看进程打开的临时文件:
[root@rac0 ~]# lsof |grep "^ed"|grep tmp ed 17874 root 3u REG 253,0 0 663197 /tmp/tmpfyDwG6P (deleted)
文件虽然被unlink了,只要进程还在,描述符和资源仍在,
那么接下来自然会想到:ed的这个临时文件,跟sed -i的临时文件有何区别
这就要了解ed的临时文件的作用了
ed的临时文件叫:scrach_file
前文有提到,ed里内存保存的数据的内存结构叫:line_buffer
源码定义结点如下:
typedef struct line /* Line node */ { struct line * q_forw; struct line * q_back; long pos; /* position of text in scratch buffer */ int len; /* length of line */ } line_t;
这是一个双向链表,整个链表代表着内存中保存的所有行,链表中的一个结点代表着一行记录
每个结点保存着行的起始位置(pos)跟长度,
这里的pos是起始位置,对应的便是scrach_file
ed正是对line_buffer+scrach_file进行编辑
我们通过对ed进行strace来看看ed的内部运行
1:准备文件aaa,有六行记录
[root@rac0 tmp]# cat -n aaa 1 1 2 2 3 3 4 4 5 5 6 6
2:用ed aaa
[root@rac0 tmp]# ed aaa 12
3:另开一个终端,strace 进程
[root@rac0 tmp]# ps -ef |grep ed root 18133 15769 0 10:20 pts/5 00:00:00 ed aaa ... [root@rac0 tmp]# strace -p 18133 Process 18133 attached - interrupt to quit read(0,
刚开始执行ed的时候,程序会处理中断输入状态
从临时文件大小可以看出来,此时临时文件大小为0,已经创建,但未写入
似乎并没有将内容写入到临时文件里,因为数据大的话,这种情况不存在,
猜测是由于文件太小了,这里调用的是标准io,数据还在缓冲里,
4:在ed里执行命令3p,打印第三行,并观察strace
[root@rac0 tmp]# ed aaa 12 3p 3
fstat64(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 5), ...}) = 0 read(0, "3", 1) = 1 read(0, "p", 1) = 1 read(0, "/n", 1) = 1 write(3, "123456", 6) = 6 _llseek(3, 0, [0], SEEK_SET) = 0 read(3, "123456", 4096) = 6 fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 5), ...}) = 0 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7c73000 write(1, "3/n", 2) = 2 read(0,
文件描述符3指向的是scrach_file,
执行了第一条命令后,内容才被写进了scrach_file,然后又读了出来
5:替换第三行第一个字符为“ddd",然后输出
[root@rac0 tmp]# ed aaa 12 ? 3p 3 3s/./ddd ddd 3p ddd
观察strace结果:
read(0, "3", 1) = 1 read(0, "s", 1) = 1 read(0, "/", 1) = 1 read(0, ".", 1) = 1 read(0, "/", 1) = 1 read(0, "d", 1) = 1 read(0, "d", 1) = 1 read(0, "d", 1) = 1 read(0, "/n", 1) = 1 open("/usr/lib/gconv/gconv-modules.cache", O_RDONLY) = 4 fstat64(4, {st_mode=S_IFREG|0644, st_size=25462, ...}) = 0 mmap2(NULL, 25462, PROT_READ, MAP_SHARED, 4, 0) = 0xb7c6c000 close(4) = 0 _llseek(3, 6, [6], SEEK_SET) = 0 fstat64(3, {st_mode=S_IFREG|0600, st_size=6, ...}) = 0 _llseek(3, 0, [0], SEEK_SET) = 0 read(3, "123456", 4096) = 6 write(3, "ddd", 3) = 3 _llseek(3, 0, [0], SEEK_SET) = 0 read(3, "123456ddd", 4096) = 9 write(1, "ddd/n", 4) = 4 read(0, "3", 1) = 1 read(0, "p", 1) = 1 read(0, "/n", 1) = 1 _llseek(3, 9, [9], SEEK_SET) = 0 write(1, "ddd/n", 4) = 4
可以看到,原来第三行的内容“3”,还在scrach_file文件里,
而替换完了的第三行新内容"ddd"实际上追加到了scrach_file文件的后边了
由此可见,ed的临时文件scrach_file的作用,是配合内存中的line_buffer,保存文件镜象内容
ed在行读写时,都是根据line_buffer里的行起始和行长度,从scratch_file进行读写的
比如指定行输出(一行或多行),对应源码里这个函数:
/* print a range of lines to stdout */ bool display_lines( int from, const int to, const int gflags ) { line_t * const ep = search_line_node( inc_addr( to ) ); line_t * bp = search_line_node( from ); if( !from ) { set_error_msg( "Invalid address" ); return false; } while( bp != ep ) { const char * const s = get_sbuf_line( bp ); if( !s ) return false; set_current_addr( from++ ); put_tty_line( s, bp->len, gflags ); bp = bp->q_forw; } return true; }
其中,get_sbuf_line这个函数便是进行scratch_file的读写
/* get a line of text from the scratch file; return pointer to the text */ char * get_sbuf_line( const line_t * const lp )
这么频繁的文件读写,效率会高吗?
这其实是一种unix编程的哲学,unix/linux的shell应用程序都有其编写原则
比如:如果处理的是行对象,则没必要在内存中保存所有的数据内容,保需要在内存中保存相关行数据以进行处理
因为这样子提高了程序的应用范围
不会导致程序在处理大数据量文件时候大量占用内存空间。