在who命令中介绍了如何读文件,接下来要通过cp命令来学习如何写文件。
(11)编写cp
问题1:cp命令能做些什么:cp能够复制文件,典型的用法是: cp source - file target - file
如果target-file所指定的文件不存在,cp就创建这个文件,如果已经存在就覆盖,target-file的内容与source-file相同。
问题2:cp命令是如何创建/重写文件的?
1.创建/重写文件
创建或重写文件的一种方法是使用系统调用函数creat。
creat的用法如下:creat告诉内核创建一个名为filename的文件,如果这个文件不存在,就创建它,如果已经存在,就把它的内容清空,把文件的长度设为0。
2.写文件
用write系统调用向已打开的文件中写入数据。
write这个系统调用告诉内核将内存中指定的数据写入文件,如果内核不能写入或写入失败,write返回-1,如果写入成功,则返回写入的字节数。
为什么实际写入的字节数会少于所要求的呢?有两个原因,第一个是有的系统对文件的最大尺寸有限制,第二个是磁盘空间接近满了。
在上述两种情况下,内核都会尽量把数据往文件中写,并将实际写入的字节数返回,所以调用write后都必须检查返回值是否与要写入的相同,如果不同,就要采取相应的措施。
问题3:编写cp1.c
文件在磁盘上,源文件在左边,右边的是目标文件,进程在用户空间,缓冲区是进程内存的一部分,进程有两个文件描述符,一个指向源文件,一个指向目标文件,从源文件中读取数据写入缓冲,再将缓冲中的数据写入目标文件。下面就是实现上述逻辑的代码:
编辑图2.4显示了涉及的对象及数据流的走向。
编辑文件在磁盘上,源文件在左边,右边的是目标文件,进程在用户空间,缓冲区是进程内存的一部分,进程有两个文件描述符,一个指向源文件,一个指向目标文件,从源文件中读取数据写入缓冲,再将缓冲中的数据写入目标文件。下面就是实现上述逻辑的代码:
/*cp1.c*/#include<fcntl.h>#include<stdlib.h>#include<unistd.h>#include<stdio.h>#define PAGESIZE 4096#define COPYMODE 0644voidoops(char *str1, char *str2);intmain(int argc, char *argv[]){int sourcefd,destinationfd;int num;char buf[PAGESIZE];if (argc != 3) {fprintf(stderr, "usage: %s source destination\n", argv[0]);exit(1); }if ((sourcefd = open(argv[1], O_RDONLY)) == -1) {oops("cannot open ", argv[1]); }if ((destinationfd = creat(argv[2], COPYMODE)) == -1) {oops("cannot open ", argv[2]); }while ((num = read(sourcefd, buf, PAGESIZE)) > 0) {if ((write(destinationfd, buf, num)) != num) {oops("cannot write to ", argv[2]); } }if (num == -1) {oops("cannot read from ", argv[1]); }if (close(sourcefd) == -1 || close(destinationfd) == -1) {oops("Error close file", ""); }return 0;}voidoops(char *str1, char *str2){fprintf(stderr, "Error: %s ",str1);perror(str2);exit(1);}
cp1.c中定义了BUFFERSIZE这个常量,用于标识每次读/写操作的数据长度,这里的值是4096,接下来是个很重要的问题:缓冲区的大小对性能有影响吗?
(12)缓冲区的大小对性能的影响
缓冲区的大小对性能有很大的影响,举例来说,用勺子把汤从一个碗里舀到另一个碗里,用较大的勺子就可以少舀几次,从而节省时间。
对文件操作而言也是这样的,来看对一个2500字节的文件的copy操作:文件大小=2500字节。如果缓冲区大小=100字节,那么需要25次read()和25次write();如果缓冲区大小=1000字节,那么需要3次read()和3次write()。把缓冲区从100字节增加到1000字节会使系统调用的次数从50次减少到6次,这确实很可观。
缓冲区的大小影响系统调用的次数,系统调用几乎影响程序的执行时间。对应着复制一个5MB大小的文件,不同的缓冲区所对应的执行时间如下:
编辑(13)为什么系统调用需要很多时间?
参见图2.5所示的控制流程。
编辑图2.5中,内核把持着对磁盘、终端、打印机等设备的访问。程序cp1.c要读取磁盘上的数据只能通过系统调用read,而read的代码在内核中,所以当read调用发生时,执行权会从用户代码转移到内核代码,执行内核代码是需要时间的。
系统调用的开销大不仅仅是因为要传输数据,当运行内核代码时,CPU工作在管理员(supervisor,又称超级用户)模式,这对应于一些特殊的堆栈和内存环境,必须在系统调用发生时建立好。系统调用结束后(read返回时),CPU要切换到用户模式,必须把堆栈和内存环境恢复成用户程序运行时的状态,这种运行环境的切换要消耗很多时间。在运行时刻,系统会根据需要不断地在两种模式间切换。
举个影片超人的例子,当肯特(生活中的超人)要从用户模式(普通人)切换到管理员模式(超人)时,他得先找个地方,比如电话亭,脱下西装,摘掉眼镜,再改变发型,变成超人后才能去拯救别人,事情完了以后,还得找个地方变回普通人。变来变去是需要时间的,要是肯特整天忙于变来变去,就不会有太多的时间来拯救人类了。
程序也是一样,所以要尽可能地减少模式间的切换。
(14)在who2.c中运用缓冲技术
在who2.c中加入缓冲机制可以提高程序的运行效率。
修改原来的主函数main,通过调用 utmp_next来取得数据,当缓冲区的数据都被取出后,utmp_next会调用read,通过内核再次获得16条记录充满缓冲区。用这种方法可以使read的调用次数减少到原来的1/16。
/*who3.c*/#include<utmp.h>#include<fcntl.h>#include<unistd.h>#include<stdio.h>#include<stdlib.h>#include<time.h>voidshow_info(struct utmp *utbufp);intmain(void){ int utmpfd; struct utmp *utbufp; if ((utmpfd = utmp_open(UTMP_FILE)) == -1) { perror("can't open UTMP_FILE"); exit(-1); } while ((utbufp = utmp_next()) != (struct utmp *)NULL) { show_info(utbufp); } utmp_close(); return 0;}voidshow_info(struct utmp *utbufp){ if (utbufp->ut_type != USER_PROCESS) return; printf("%-8.8s", utbufp->ut_name); printf(" "); printf("%-8.8s", utbufp->ut_line); printf(" "); printf("%12.12s", ctime((const time_t *)&(utbufp->ut_time)) + 4); printf(" "); printf("(%s)", utbufp->ut_host); printf("\n");}
用一个能容纳16个utmp结构的数组作为缓冲区,在图2.6中标识为buffer,就像你次会买很多个鸡蛋一样,buffer可以存放很多数据。编写utmp_next函数来从缓冲区中取得下一个utmp结构的数据。以上算法在utmplib.c中加以实现。
编辑/*utmplib.c*/#include<utmp.h>#include<fcntl.h>#include<unistd.h>#define UTSIZE 16#define UTMPSIZE sizeof(struct utmp)static int utmp_fd = -1;static int curr_bufp;static int num_bufp;static char buf[UTSIZE * UTMPSIZE];staticintutmp_reload(void);intutmp_open(char *filename){ utmp_fd = open(filename, O_RDONLY); num_bufp = 0; curr_bufp = 0;return utmp_fd;}struct utmp* utmp_next(void) {struct utmp *recp;if (utmp_fd == -1)return NULL;if (curr_bufp == num_bufp && utmp_reload() == 0) return NULL; recp = (struct utmp *)&buf[curr_bufp * UTMPSIZE]; curr_bufp++;return recp;}staticintutmp_reload(void){int num; num = read(utmp_fd, buf, UTSIZE * UTMPSIZE); curr_bufp = 0; num_bufp = num / UTMPSIZE;return num_bufp;}voidutmp_close(void){if (utmp_fd != -1) close(utmp_fd);}
(15)内核使用缓冲吗?
磁盘的I/O操作消耗的时间更多,为了提高效率,内核也使用缓冲技术来提高对磁盘的访问速度,如图2.7所示。
编辑正如utmp文件是用户登录记录的集合,磁盘是数据块的集合,内核会对磁盘上的数据块作缓冲,就像who程序缓冲utmp记录一样。内核将磁盘上的数据块复制到内核缓冲区中,当一个用户空间中的进程要从磁盘上读数据时,内核一般不直接读磁盘,而是将内核缓冲区中的数据复制到进程的缓冲区中。
当进程所要求的数据块不在内核缓冲区时,内核会把相应的数据块加入到请求数据列表中,然后把该进程挂起,接着为其他进程服务。一段时间之后(很短),内核把相应的数据块从磁盘读到内核缓冲区,然后再把数据复制到进程的缓冲区中,最后唤醒被挂起的进程。
理解内核缓冲技术的原理有助于更好地掌握系统调用read和write,read把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区,它们并不等价于数据在内核缓冲和磁盘之间的交换。
从理论上讲,内核可以在任何时候写磁盘,但并不是所有的write操作都会导致内核的写动作。内核会把要写的数据暂时存在缓冲区中,积累到一定数量后再一次写入。有时会导致意外情况,比如突然断电,内核还来不及把内核缓冲区中的数据写到磁盘上,这些更新的数据就会丢失。
应用内核缓冲技术导致的结果:提高磁盘I/O效率、优化磁盘的写操作、需要及时地将缓冲数据写入盘。
(16)文件读写
who是从文件读数据,cp从一个文件读数据写入到另一个文件中,会不会有对同一个文件既读又写的情况呢?
(17)以注销过程为例
这其实很简单,要把用户名清空,按以下步骤做就行了:1.打开文件utmp;2.从utmp中找到包含你所在终端的登录记录;3.对当前记录做修改;4.关闭文件。
负责注销的程序修改当前记录;再把它写回到文件 utmp中。具体来说,要把 ut_type的值从USER_PROCESS改成 DEAD_PROCESS;把 ut_time字段的值改为注销时间,也就是当前时间。
那如何把修改过的记录写回文件?可以用write吗?不行,write只会更新下一条记录,而不是当前那条要修改的记录。因为系统每次打开一个文件都会保存一个指向文件当前位置的指针,当读写操作完成时,指针会移到下一个记录位置,这个指针与文件描述符相关联。在这种情况下,指针是指向下一条登录记录的头一个字节,这引出了一个重要的问题:在文件操作中,如何改变一个文件的当前读/写位置?答题:使用系统调用lseek。
(18)改变文件的当前位置
Unix每次打开一个文件都会保存一个指针来记录文件的当前位置,如图2.8所示。
编辑当从文件读数据时,内核从指针所标明的地方开始,读取指定的字节,然后移动位置指针,指向下一个未被读取的字节,写文件的操作也是类似的。
指针是与文件描述符相关联的,而不是与文件关联,所以如果两个程序同时打开一个文件,这时会有两个指针,两个程序对文件的读操作不会互相干扰。
lseek改变文件描述符所关联的指针的位置,新的位置由dist和base来指定,base是基准位置,dist是从基准位置开始的偏移量。基准位置可以是文件的开始(0)、当前位置(1)或文件的结尾(2)。
(19)终端注销的代码
int logout_tty(char *line) { int fd; struct utmp rec; int len = sizeof(struct utmp); int retval = -1; if ((fd = open(UTMP_FILE, O_RDWR)) == -1) return -1; while (read(fd, &rec, len) == len) { if (strncmp(rec.ut_line, line, sizeof(rec.ut_line)) == 0) { rec.ut_type = DEAD_PROCESS; if (time(&rec.ut_time) != -1) { if (lseek(fd, -len, SEEK_CUR) != -1) { if (write(fd, &rec, len) == len) { retval = 0; } } } break; } } if (close(fd) == -1) retval = -1; return retval;}
(20)处理系统调用中的错误
如果open无法打开指定的文件,它会返回-1。同样地,当read无法读的时候,它会返回-1,当lseek无法指定指针位置时,它也会返回-1,-1是表示在系统调用中出了些问题,调用者每次都必须检查返回值,一旦检测到错误,必须做出相应的处理。
(21)确定错误的种类:errno
内核通过全局变量errno来指明错误的类型,每个程序都可以访问到这个变量。在error(3)的联机帮助和<errno.h>中包含错误代码和相应的说明,以下是一些例子:
#define EPERM 1 /* Operation not permitted */#define ENOENT 2 /* No such file or directory */#define ESRCH 3 /* No such process */#define EINTR 4 /* Interrupted system call */#define EIO 5 /* I/O error */
(22)不同的错误需要不同的处理
根据以上列出的错误类型,应该在程序中进行相应的处理:
intsample(){ int fd; fd = open("file", O_RDONLY); if(fd == -1) { printf("Cannot open file:"); if ( errno == ENOENT ) printf("There is no such file."); else if(errno == EINTR) printf("Interrupted while opening file."); else if ( errno == EACCES ) // 修复:EACCESS → EACCES printf("You do not have permission to open file."); } return 0; // 补充:int函数必须有返回值}
(23)显示错误信息:perror(3)
另外一种更简便的方法是用 perror(string)这个函数,它会自己查找错误代码,在标准错误输出中显示出相应的错误信息,参数string是要同时显示出的描述性信息。
应用了perror的sample:
intsample(){ int fd; fd = open("file", O_RDONLY); if (fd == -1) { perror("Cannot open file"); return -1; } close(fd); return 0;}
当有错误发生时,可能会看到如下的信息:
Cannot open file: No such file or directoryCannot open file: Interrupted system call
显示的第一部分是用户传递进去的描述性信息,第二部分是根据错误代码查到的错误提示。