19、其他文件编程函数(目录文件、链接文件、临时文件)
1、目录文件
Linux系统的最常见的问题就是扫描目录,也就是确定一个特定目录下存放的文件,在Shell设计中很容易做到,只让Shell做一次表达式的通配符扩展。在过去,Unix系统的各种变体都允许用户通过编程访问底层文件系统结构,把目录当作一个普通文件打开,并直接读取目录数据项,但不同的文件系统结构及其实现已经使这种方法没有什么可移植性了,现在,一套标准的库函数已经把被开发出来,使得目录的扫描工作变得十分简单。
mkdir()和mkdirat()
函数mkdir()和mkdirat()用于创建目录,其函数原型如下:
#include <fcntl.h> #include <sys/stat.h> int mkdir(const char *path,mode_t mode); int mkdirat(int dirfd,const char *pathname, mode_t mode);
path是目录名
mode是目录权限
返回值:
返回0 表示成功, 返回 -1表示错误,并且会设置errno值。
mode模式位:
mode 表示新目录的权限,这里需要对文件权限进行一个基本的理解。
这两个函数用于创建一个新的空目录。其中"."和".."是目录项自动创建的,所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。
常见的错误是制定于文件相同的mode(只指定读、写权限。),但是,对于目录通常至少要设置一个执行权限位,以允许访问该目录中的文件名。
函数mkdirat()和mkdir()类似。参数dirfd具有特殊值AT_FDCWD。参数pathname指定了绝对路径名时,函数mkdirat()与mkdir()完全一样,否则,dirfd()是一个打开的目录,相对路径名根据此目录进行计算。
示例:使用mkdir()创建目录
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> #include<fcntl.h> typedef int fid_t; int main() { fid_t fd=0; int val=0; fd=open("/home/li/linux_c",O_RDONLY|O_DIRECTORY); if(fd<0) { perror("open()"); exit(1); } val=mkdirat(fd,"mydir",0755); if(val<0) { perror("mkdir()"); exit(1); } close(fd); exit(1); }
rmdir()
函数rmdir()可以删除一个空目录。空目录是只包含"."和".."这两项的目录,其函数原型如下:
#include <unistd.h> int rmdir(const char*path);
调用此函数使目录的链接计数为0,并且在没有其他进程打开此目录的情况下,释放此目录占用的空间。如果链接计数达到0,有一个或多个进程打开此目录,则在此函数返回前删除最后一个链接及"."和"..",在此目录中不能再创建新文件,但是在最后一个进程关闭它之前不能释放此目录(即使另一些进程打开此目录,它们在此目录下也不能执行其他操作。这样处理是为了使函数rmdir()能成功执行,该目录必须是空的)。
opendir()、closedir()与readdir()
由于目录文件本身也是一种文件,因此用户可以像打开普通文件那样将其打开。不过,打开/关闭一个目录文件需要使用系统提供的特殊接口,而不能使用打开/关闭普通文件的函数open()/close()。Linux提供了单独的函数来进行处理,其原型如下:
#include <sys/types.h> #include <dirent.h> DIR *opendir(const char*name); DIR *opendir(int fd); int closedir(DIR *dirp);
函数opendir()的参数表示需要打开目录的路径名,器返回值是一个DIR结构的指针。该结构是一个内部结构,用于保存所打开目录的信息,其作用类似于FILE结构。如果函数opendir()出错,则会返回NULL。函数fdopendir()最早出现在SUSv4中,它提供了一种方法,可以把打开的文件描述符转化为目录处理啊函数所需的DIR结构。
函数closedir()的参数表示需要关闭的目录的DIR结构指针,关闭后则不能再应用该目录。成功返回0,否则返回-1。
Linux环境下允许用户像读取文件内容一样读取目录存放文件的目录项,在读取这些目录项的同时会改变目录文件的当前读写位置。Linux环境下不提供对目录文件进行写操作的函数,也就是说,只有创建一个新文件或操作目录时,才可能项目录文件中添加一个目录项,从而完成对目录文件的写操作。Linux系统提供函数readdir()读取一个目录中的内容,其函数原型如下:
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
函数readdir()返回一个指针,该指针指向dirent类型结构,该结构里保存着目录流dirp中的下一个目录的有关资料,后续调用函数readdir()将返回后续的目录项。如果发生错误或者到达目录结尾,函数readdir()将返回NULL。
dirent类型结构在头文件<bits/dirent.h>中定义,具体内容如下
Struct dirent { ino_t d_ino; //此目录进入点的inode ff_t d_off; //目录文件开头到此目录进入点的位移 signed short int d_reclem; //_name的长度,不包含NULL字符 unsigned char d_type; // d_name所指的文件类型 char d_name[256]; //文件名 };
下面看一下简单的实现代码示例:显示目录下的所有文件
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <dirent.h> #include <errno.h> DIR *opendir(const char *name); int main(int argc, char *argv[]) { int ret=0; int count=0; DIR *dp; struct dirent *dirent; if(argc!=2) { printf("usage:readdir<staring-pathname>\n"); exit(1); } dp=opendir(argv[1]); if(dp==NULL) { perror("opendir()"); exit(1); } while((dirent=readdir(dp))!=NULL) { count++; printf("file[%2d]:->%s\n",count,dirent->d_name); } if(errno) { perror("readdir()"); exit(1); } exit(0); }
编译运行结果如下:
getcwd()
每个进程都有一个工作目录,如果程序使用了相对路径,进程就会以当前目录为起点开始搜索相对路径。也就是说,在程序中所有以文件名引用的文件路径都被解释为当前工作目录或文件命名。
例如,进程的工作目录为/home/instructor/code,程序中有如下语句:
fd=open("test.txt",O_RDONLY"); fd=open("./tmp/test.txt",O_RDONLY);
第一个函数open()打开的文件为/home/instructor/code/test.txt,而第二个函数open()打开的文件为/home/instructor/code/tmp/test.txt
在编程过程中,用户可以使用函数getcwd()来得到当前进程的工作目录。事实上,Linux系统命令pwd就是通过调用该函数来得到Shell环境下的当前工作目录的,函数getcwd()原型如下:
#include <unist.h> char *getcwd(char *buf,size_t size);
函数getcwd()用于获取当前进程的绝对路径。他需要两个参数:参数buf用于传递缓冲区地址;参数size用于传递缓冲区长度。该缓冲去必须有足够的长度以容纳绝对路径名再加上一个终止符NULL,否则返回出错。
执行成功会返回缓冲区的地址,也就是参数buf的地址;执行失败,会返回NULL,同时errno被设置为对应的值。
chdir()
程序可以像用户在文件系统里那样来浏览目录,就像在Shell中使用命令cd来切换目录一样,程序可以通过使用chdir()来该变当前工作目录,其函数原型如下:
#include <unistd.h> int chdir(const char *path);
参数path用于指定要切换到的目录,可以为相对路径,也可以为绝对路径。函数执行成功时返回0;否则,返回-1,同时设置errno为相应的值。
下面展示一个切换工作目录的实例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define SIZE 1024 int main(int argc,char argv[]) { char *buf; buf=(char*)malloc(SIZE*sizeof(char)); memset(buf,0,SIZE*sizeof(char)); if(getcwd(buf,SIZE)==NULL) { perror("getcwd()"); exit(1); } printf("before change directory:%s\n",buf); if(chdir("/var/spool")<0) { perror("chdir()"); exit(1); } memset(buf,0,SIZE*sizeof(char)); if(getcwd(buf,SIZE)==NULL) { perror("getcwd()"); exit(1); } printf("after change directory:%s\n",buf); }
编译运行结果
2、链接文件
链接是在共享文件和访问该文件的目录项之间建立联系的一种方法。为解决文件的共享使用,Linux 系统引入了两种链接:硬链接 (hard link) 与软链接(又称符号链接,即 soft link 或 symbolic link)。链接为 Linux 系统解决了文件的共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若一个 inode 号对应多个文件名,则称这些文件为硬链接。换言之,硬链接就是同一个文件使用了多个别名。
索引节点(inode结点)
我们知道文件都有文件名与数据,这在 Linux 上被分成两个部分:
-
用户数据 (user data), 即文件数据块 (data block),数据块是记录文件真实内容的地方;
-
元数据 (metadata),是文件的附加属性,如文件大小、创建时间、所有者等信息。
在 Linux 中,元数据中的 inode 号(inode 是文件元数据的一部分但其并不包含文件名,inode 号即索引节点号)才是文件的唯一标识而非文件名。文件名仅是为了方便人们的记忆和使用,系统或程序通过 inode 号寻找正确的文件数据块。
简单的说这个inode即使文件在一个文件系统中的唯一标识,需要访问这个文件的时候必须先找到并读取这个文件的 inode。
Inode 里面存储了文件的很多重要参数,其中唯一标识称作 inumber, 其他信息还有创建时间(ctime)、修改时间(mtime) 、文件大小、属主、归属的用户组、读写权限等信息。
查看inode
号可使用命令stat
或ls -i
查看 inodes 使用情况的命令 df -i
知道了inode的作用,我们就可以深入理解软硬链接的区别了。
-
软连接相当于快捷方式,如果打开并修改软连接,相应的文件也会随之改变。但是如果删除软连接,源文件并不会受到影响。
-
硬链接有点像引用和指针的结合,当打开和修改它时,相应的文件随之改变,但是所有这个文件的硬链接的内容也随之改变,这是因为所有的硬链接都拥有唯一的一个 inode 号,他们指向的是同一文件。
-
软连接可以跨文件系统创建,也就是可以在某个分区中创建到另外一个分区的软连接
-
硬链接则只能在本文件系统中使用(想想为什么?),其实原理很简单,因为 inode 是这个文件在当前分区中的索引值,是相对于这个分区的,当然不能跨越文件系统了。
-
最后一个区别是软连接可以连接任何文件或者文件夹,而硬链接则只能在文件之间创建
硬链接
硬链接本质上是一个指针,指向目标文件的索引节点。一个目录项只能对应一个索引节点,而一个索引节点可以对应多个目录项。因此,一个目录项只有一个链接,而一个索引节点可以有多个链接,如图所示:
Linux系统提供了系统调用函数link()和unlink(),以方便用户在程序中能够创建和删除硬链接,其函数原型如下:
#include <unistd.h> int link(const char *oldpath,const char *newpath); int unlink(const char *pathname);
函数link()用于创建一个新的由参数newpath指定的目录项,该目录项引用由参数oldpath指定的已有目录项。函数执行车成功后,新目录项只创建参数newpath的最后一个分量,路径中的其他部分应当已经存在。
函数执行成功返回0;否则返回-1,同时errno被设置为相应的值。
函数unlink()用于删除由参数pathname指定的目录项,并将其引用的文件的链接计数减1。如果该文件还有其他链接,则仍可通过这些链接访问数据。
示例:创建和删除硬链接
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <sys/stat.h> #define BUFSIZE 1024 int main(int argc,char *argv[]) { int fd=0; struct stat statbuf; char buf[BUFSIZE]; // 通过文件名test.txt获取文件信息,并保存在buf所指的结构体stat中 if(stat("test.txt",&statbuf)==-1) { perror("status()"); exit(1); } printf("test.txt, the number of links is:%d\n",statbuf.st_nlink); //将test.txt的索引节点了连接到test2.txt if(link("test.txt","test2.txt")==-1) { perror("link()"); exit(1); } //获得test.txt文件属性 if(stat("test.txt",&statbuf)==-1) { perror("status()"); exit(1); } printf("test.txt, the number of links is:%d\n",statbuf.st_nlink); //获得test2.txt文件属性 if(stat("test2.txt",&statbuf)==-1) { perror("status()"); exit(1); } printf("test2.txt, the number of links is:%d\n",statbuf.st_nlink); //打开test.txt文件 if((fd=open("test.txt",O_RDWR))==-1) { perror("open()"); exit(1); } memset(buf,0,BUFSIZE); strcpy(buf,"hello world"); //写test.txt文件 if(write(fd,buf,strlen(buf))==-1) { perror("write()"); exit(1); } close(fd); //打开test2.txt文件 if((fd=open("test2.txt",O_RDONLY))==-1) { perror("open()"); exit(1); } memset(buf,0,BUFSIZE); //读test2.txt文件 if(read(fd,buf,1024)==-1) { perror("read()"); exit(1); } printf("content of test2.txt is:%s\n",buf); close(fd); //删除test2.txt的链接 if(unlink("test2.txt")==-1) { perror("unlink()"); exit(1); } if(stat("test.txt",&statbuf)==-1) { perror("stat()"); exit(1); } printf("test.txt, the number of links is:%d\n",statbuf.st_nlink); //获得test.txt文件属性 if((fd=open("test.txt",O_RDWR))==-1) { perror("fail to open"); exit(1); } //删除test.txt的链接 if(unlink("test.txt")==-1) { perror("unlink()"); exit(1); } //获得相对路径的文件统计信息 if(fstat(fd,&statbuf)==-1) { perror("fstat()"); exit(1); } printf("test.txt, the number of links is:%d\n",statbuf.st_nlink); //读test2.txt的内容 if(read(fd,buf,1024)==-1) { perror("read()"); exit(1); } printf("content of test2.txt is:%s\n",buf); close(fd); exit(0); }
创建一个test.txt文件后编译运行文件可以得到如下结果:
值得注意的是,硬链接有以下限制:
-
不能对目录创建硬链接
-
不能对不同的文件系统创建硬链接,即两个文件名要在相同的文件系统下。
-
不能对不存在的文件创建硬链接,由原理即可知原因。
软链接
硬链接不能跨越多个文件系统,因为索引节点编号在自己的文件系统外没有任何意义,为了能够跨越文件系统建立链接,linux系统实现了符号链接,也称软链接。
(软链接的作用是避开目录之间不能使用硬连接,不同系统之间不能使用硬连接的限制。)
软链接是指一个文件的间接指针,其文件内容主要用于记录目标文件的存储路径,软链接就是一个普通文件,只是数据块内容有点特殊。软链接有着自己的 inode 号以及用户数据块。其链接模型如下:
软链接提供了一个访问目标文件的快捷途径,其作用类似于Windows系统中的快捷方式,当用户需要访问一个文件时,不经过该文件的路径也可访问,只要建立一个指向该文件的软链接,当用户操作这个软链接时,就等于操作软链接所指向的文件。
对符号连接以及它指向的对象并无任何文件系统限制,任何用户都可以创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录结构移到系统中的另一位置处。
Linux提供了系统调用函数symlink()和readlink(),以方便用户在程序中能够创建和读取软链接,其函数原型如下:
#include <unistd.h> int symlink(const char *oldpath,const char * newpath); ssize_t readlink(const char *path,char *buf ,size_t bufsiz);
函数symlink用于创建一个指向oldpath的新目录项newpath。在创建此软链接时,并不要求newpath已经存在,并且oldpath 与newpath并不需要位于同一操作系统中。
函数readlink()组合了函数open()、read()、和close()等的所有操作。
如果函数执行成功,返回buf的字节数,zaibuf中返回的软链接内容不以字符NULL结束。
示例:创建软链接
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <string.h> #define BUFSIZE 1024 int main() { int fd=0; char buf[BUFSIZE]; if(symlink("test.txt","sym1.txt")==-1) { perror("symlink()"); exit(1); } if(symlink("sym1.txt","sym2.txt")==-1) { perror("symlink()"); exit(1); } fd=open("sym2.txt",O_RDONLY); memset(buf,0,BUFSIZE); if(read(fd,buf,BUFSIZE)==-1) { perror("read()"); exit(1); } printf("read()->sym2.txt:%s\n",buf); close(fd); memset(buf,0,BUFSIZE); if(readlink("sym1.txt",buf,BUFSIZE)==-1) { perror("readlink()"); exit(1); } printf("readlink()->sym1.txt:%s\n",buf); memset(buf,0,BUFSIZE); if(readlink("sym2.txt",buf,BUFSIZE)==-1) { perror("readlink()"); exit(1); } printf("readlink()->sym2.txt:%s\n",buf); exit(0); }
创建一个test.txt文件后编译运行程序有如下结果。
软链接的特征
-
可以对目录创建软链接,遍历操作会忽略目录的软链接。
-
可以跨文件系统
-
可以对不存在的文件创建软链接,因为放的只是一个字符串,至于这个字符串是不是对于一个实际的文件,就是另外一回事了
-
硬链接原文件/链接文件公用一个inode号,说明他们是同一个文件,而软链接原文件/链接文件拥有不同的inode号,表明他们是两个不同的文件;
-
在文件属性上软链接明确写出了是链接文件,而硬链接没有写出来,因为在本质上硬链接文件和原文件是完全平等关系;
-
链接数目是不一样的,软链接的链接数目不会增加;
-
文件大小是不一样的,硬链接文件显示的大小是跟原文件是一样的。而这里软链接显示的大小与原文件就不同了
-
硬链接不能跨越文件系统,且不能指向目录,但是软链接没有这些限制
3、临时文件
程序运行时,有时需要存储一些临时数据,这便需要用到临时文件。linux为创建临时文件提供了两套实现方案。
1、ISO C库
ISO C标准IO库提供了函数tmpnam()和tmpfile(),用以完成临时文件的创建,其函数原型如下:
#include <stdio.h> char *tmpnam(char *s); FILE *tmpfile(void);
tempnam函数是tmpnam的一个变体,允许调用者指定目录和生成路径名的前缀。directory有四种可能的选择,第一个满足的情况将作为这个目录:
1、如果环境变量TMPDIR定义了,它被作为这个目录。
2、如果directory不为NULL,它被作为目录。
3、里的字符串P_tmpdir被作为这个目录。
4、一个本地目录,通常是/tmp,被作为这个目录。
函数tmpnam()产生一个与现有文件名不同的有效路径名字符串,每次调用它时,都会产生不同路径名,最多调用次数为TMP_MAX。TMP_MAX在<bits/stdio_lim.h>中定义如下:
#define TMP_MAX 238328
函数tmpname()的参数若为NULL,则产生的路径名保存在一个静态区中,指向该静态去的指针作为函数值返回。后续调用函数tmpname()时,会重写该静态区(这意味着,如果我们多次调用此函数,而且想保存路径名时,应当保存路径名的副本,而不是指针的副本)。若参数s不为NULL,则认为它应当指向的长度至少是L_tmpnam个字符的数组,所产生的路径名存放在该数组中,数组地址也作为函数值返回。
函数tmpfile()创建一个临时的二进制文件(类型为”wb+"),在关闭该文件或结束程序时会自动删除文件。linux系统对二进制文件不会进行特殊区分。
示例:ISO C方式创建临时文件
#include <stdio.h> #define MAXLINE 4096 int main(void) { char name[L_tmpnam], line[MAXLINE]; FILE *fp; printf("%s\n", tmpnam(NULL)); /*生成第一个临时文件 */ tmpnam(name); /* 生成第二个临时文件*/ printf("%s\n", name); if ((fp = tmpfile()) == NULL) /* 创建临时文件 */ exit(1); fputs("one line of output\n", fp); /* 写临时文件*/ rewind(fp);//将文件指针重新指向一个流的开头。 if (fgets(line, sizeof(line), fp) == NULL) exit(1); fputs(line, stdout); /* 输出我们的写入行*/ exit(0); }
编译运行如下:
函数tmpfile()经常使用的是先调用tmpname()产生位移的路径名,然后用改路径名创建一个文件,并且立即unlink它。对一个文件解除链接时不会删除其内容,关闭文件时才会删除其内容,而关闭文件是显式的,可以在程序终止时自动进行。
2、Single UNIX Specification
Single UNIX Specification 为处理临时文件时定义的另外两个函数:mkdtemp()和mkstemp()。他们是XSI的扩展部分,其函数原型如下:
#include <stdlib.h> char mkdtemp(char *template); int mkstemp(char *template);
函数mkdtemp()用于创建一个临时目录,该目录有为唯一的名字;函数mkstemp()用于创建一个文件,该文件有唯一的名字。
名字是通过template进行选择的,参数template是一个字符串,其后6位必须设置为"XXXXXX",函数会将这些占位符替换成不同的字符来构建唯一的路径名。如果成功的话,这两个函数将修改template字符串以反映临时文件的名字。
有函数mkdtemp()创建的目录使用的访问权限集为:S_IRUSR|S_IWUSR|S_IXUSR。调用进程的文件模式创建屏蔽字可以进一步限制这些权限,如果目录创建成功,函数mkdtemp()返回新目录的名字。
函数mkstemp()以唯一的名字创建一个普通文件并打开该文件,该函数返回的文件描述符以读写的方式打开。由函数mkstemp()创建的文件使用访问权限:S_TRUSR|S_IWUSR。
与函数tempfile()不同,函数mkstmep()创建的临时文件不会自动消失,如果选哪个从文件系统命名空间中删除该文件,用户必须对它解除链接。
示例:Single UNIX Specification方式创建临时文件
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> void make_temp(char *template); int main(void) { char good_template[] = "/tmp/dirXXXXXX"; char *bad_template = "/tmp/dirXXXXXX"; printf("trying to create first temp file...\n"); make_temp(good_template); printf("trying to create second temp file...\n"); make_temp(bad_template); exit(0); } void make_temp(char *template) { int fd; struct stat buf; if((fd = mkstemp(template)) < 0) { printf("can't create temp file"); } printf("temp name=%s\n", template); close(fd); if(stat(template, &buf) < 0) { if(errno == ENOENT) { printf("file doesn't exit\n"); } else { printf("stat failed\n"); } } else { printf("file exists\n"); unlink(template); } }
程序运行结果如下:
使用tmpname()和tmpfile()有一个缺点:在返回唯一的路径名和用改名字创建文件之间存在一个时间窗口,在这个时间窗口中,另一进程可以用相同的名字创建文件。因此推荐使用函数mkdtemp()和mkstemp()。因为他们呢不存在这个问题。
参考链接: