学习linuxC语言有一阵子了,这里最重要最基础的部分当属文件操作部分了,所以很有必要在学习一段时间后总结一下相关知识。
1.linux文件系统最突出的一个特点就是对用户访问权限的控制,即采用ugo的访问控制形式。
2.在linuxC语言中所涉及的文件输入输出函数,比如open,creat,read等,均不是标准C下的库函数,因此用上述函数所编写的程序不方便移植。但是这些函数比标准C下所对应的输入输出函数速度要快的多,因为它们直接条用操作系统。
提到这些函数,我们就不能不提到文件描述符。这也是LinuxC与标准C在文件操作部分的一大不同点。我们可以回想在标准C下我们通常打开文件,返回的是一个FILE类型的指针;而在LinuxC下我们则返回一个整形变量——文件描述符。
3.open函数可以打开最多有三个参数。第二个参数是打开文件的方式,只能取O_RDONLY,O_WRONLY,O_RDWR中的一个。通常上面的参数还会与其他一些标志进行货运算。这里只说两种最典型的情况。
O_CREAT|O_EXCL|O_WRONLY:这种情况只适合打开一个不存在的文件;如果要打开的文件存在,那么则会返回错误标志。并且此时会用到第三个参数:存取权限。这里应该注意文件的存取权限和打开方式是两个不同的概念。具体可参看这里的文章。
O_CREAT|O_TRUNC|O_WRONLY:这种情况即适合打开一个已存在的文件,又适合打开一个不存在的文件。如果要打开的文件存在,那么会自动会清空原文件中的数据,即文件长度为0。但是文件的属性不变,也就是说第三个参数此时失效。
上面我们说到了第三个参数mode,它用来说明文件的存取权限。但是实际上存取权限是mode与umask的进行(mode&~umask)运算后的来的结果。这一点还可以参见这里的文章。
4.creat函数也可以打开或创建一个文件。与open不同的是,creat函数既可以打开一个存在的文件,又可以打开一个不存在的文件。因此才creat函数相当于这样使用open函数:open(const char* pathname,(O_CREAT|O_TRUNC|O_WRONLY),mode_t mode)。不过应该注意的,creat函数只能以只写方式打开或创建一个文件。
5.读写函数使用起来比较简单,成功调用会返回实际读(或写)的字节数。一般在使用这两个函数的同时会用if语句对返回值加以判断,以确定调用是否正常。比如具体代码可参考如下:
if((ret=read(fd,read_buf,len))<0) { my_error("read",__LINE__); }
这里并没有严格规定返回值必须等于读(或写)的字节数,因为有可能当前文件的读写指针与文件末尾的偏移量小于要读(或写)的字节数(通俗的说就是不够读或写),这种情况在不严格要求的情况下也算正常读取。
6.我们并不是希望每次都从文件头开始读或写,那么这里就要涉及到文件的读写指针的移动了。lseek函数的作用是将文件描述符为fd的文件从whence处开始移动offset个字节。不管参数如何搭配,lseek成功调用后,都会返回当前的读写位置,也就是距离文件开头的字节数。根据各个参数的含义,就会又下面的几种常用方法:
将文件读写指针移动到文件开始:lseek(fd,0,SEEK_SET);
将文件读写指针移动到文件结尾:lseek(fd,0,SEEK_END);
获取文件读写指针当前的位置:ret=lseek(fd,0,SEEK_CUR);
lseek允许将文件指针设置到文件结束符之后,但这并不改变文件的大小。如果继续对EOF之后的位置写数据,那么之前的EOF之处与之后写入的数据之间将存在一段“空隙”。如果用read读取这段间隔的数据,那么可以发现数据为0,其实这段空隙中填充的是’\0’。比如用lseek函数从文件末尾移动10个字节,再继续写入新数据,那么旧数据和新数据之间将有10个’\0’。
我们都知道字符串均以’\0’结尾,所以我们直接输出读取文件的那个字符串是不行的,因为它只会输出空隙前的数据(旧数据)。因此我们得一个字符一个字符的输出,这样才能将新旧数据都输出。但这样仍然不能输出空隙中的数据,不过用od -c filename命令就可以查看文件中的每一个字符,包括空隙中的’\0’。
7.初次学习dup函数可能不能真实体会其用途。不过涉及到输入输出重定位时,它的威力就显示了出来。比较特别的是,dup函数每次都返回系统内尚未使用的最小文件描述符。在前面我们一起分析管道应用的时候,有一个主程序监控子程序的例子。由于子程序exec后,以前那些数据(比如管道的描述符)将荡然无存,所以我们需要用dup进行一些重定位。具体来看:
switch(pid) { case -1: perror("fork failed"); exit(1); case 0: close(0); dup(fd[0]); execve("ctrlprocess",(void*)argv,environ); exit(0); default: close(fd[0]); write(fd[1],argv[1],strlen(argv[1])); break; }
可以看到父进程负责向管道写数据。而子进程脱离父进程前先将标准输入端关闭,然后再对管道的输出端dup一个文件描述符,接着就可以执行新的程序ctrlprocess了。而新的子进程只需在标准输入端读数据就可以了。
也许看到这里你还有些不明白,其实这里就用到了上文中说的返回最小的文件描述符。关闭标准输入端,那么当前系统内最小的可用文件描述符当然就是0了!而这还不是重点,当我们将管道的输出端定位到标准输入后,即便子进程去执行新的进程(原有数据段被废弃),那么至少标准输入这个文件描述符是不变的。那么子进程在标准输入里读,也就相当于在管道的读端读取数据。
而理解了上述原理,那么close(0);和dup(fd[0]);可以用一条语句完成:dup2(fd[0],0);因为0文件描述符正在使用,因此dup2函数会先关闭标准输入,然后再将管道的输出端赋值到标准输入,即此时0和fd[0]都同时指向管道。