操作系统-4.5 文件和目录

203 阅读8分钟

linux 7种文件类型

文件IO中认识的文件类型有:普通文件-,目录文件d,链接文件l。 管道文件p 设备文件: 字符设备文件c 块设备文件b 套接字设备s

普通文件,符号链接文件及目录文件不仅有inode号,还占用磁盘空间。 管道文件和套接字、字符设备文件、块设备文件不占用磁盘空间,只有innode号。

创建方式:

  • open: 普通文件
  • mkdir: 目录文件
  • ln -s: 链接文件
  • mkfifo: 创建管道
  • mknod: 字符设备文件
  • socket: 创建套接字

文件和目录操作的系统函数

stat

stat(2)函数读取文件的inode,然后把inode中的各种文件属性填入一个struct stat结构体传出给调用者。stat(1)命令是基于stat函数实现的。 stat需要根据传入的文件路径找到inode,假设一个路径是/opt/file,则查找的顺序是:

  1. 读出inode表中第2项,也就是根目录的inode,从中找出根目录数据块的位置
  2. 从根目录的数据块中找出文件名为opt的记录,从记录中读出它的inode号
  3. 读出opt目录的inode,从中找出它的数据块的位置
  4. 从opt目录的数据块中找出文件名为file的记录,从记录中读出它的inode号
  5. 读出file文件的inode

还有另外两个类似stat的函数:

  • fstat(2)函数传入一个已打开的文件描述符,传出inode信息
  • lstat(2)函数也是传入路径传出inode信息,但是和stat函数有一点不同,当文件是一个符号链接时,stat(2)函数传出的是它所指向的目标文件的inode,而lstat函数传出的就是符号链接文件本身的inode。

access/chmod/chown/utime

access(2)函数检查执行当前进程的用户是否有权限访问某个文件,传入文件路径和要执行的访问操作(读/写/执行),access函数取出文件inode中的st_mode字段,比较一下访问权限,然后返回0表示允许访问,返回-1表示错误或不允许访问。

chmod(2)和fchmod(2)函数改变文件的访问权限,也就是修改inode中的st_mode字段。这两个函数的区别类似于stat/fstat。chmod(1)命令是基于chmod函数实现的。

chown(2)/fchown(2)/lchown(2)改变文件的所有者和组,也就是修改inode中的User和Group字段,只有超级用户才能正确调用这几个函数,这几个函数之间的区别类似于stat/fstat/lstat。chown(1)命令是基于chown函数实现的。

utime(2)函数改变文件的访问时间和修改时间,也就是修改inode中的atime和mtime字段。touch(1)命令是基于utime函数实现的。

truncate

truncate(2)和ftruncate(2)函数把文件截断到某个长度,如果新的长度比原来的长度短,则后面的数据被截掉了,如果新的长度比原来的长度长,则后面多出来的部分用0填充,这需要修改inode中的Blocks索引项以及块位图中相应的bit。这两个函数的区别类似于stat/fstat。

link/symlink/readlink/unlink

link(2)函数创建硬链接,其原理是在目录的数据块中添加一条新记录,其中的inode号字段和原文件相同。 symlink(2)函数创建一个符号链接,这需要创建一个新的inode,其中st_mode字段的文件类型是符号链接,原文件的路径保存在inode中或者分配一个数据块来保存。 ln(1)命令是基于link和symlink函数实现的。

readlink(2)函数读取一个符号链接所指向的目标路径,其原理是从符号链接的inode或数据块中读出保存的数据,这就是目标路径。

unlink(2)函数删除一个链接。

  • 如果是符号链接则释放这个符号链接的inode和数据块,清除inode位图和块位图中相应的位。
  • 如果是硬链接则从目录的数据块中清除一条文件名记录,如果当前文件的硬链接数已经是1了还要删除它,就同时释放它的inode和数据块,清除inode位图和块位图中相应的位,这样就真的删除文件了。 unlink(1)命令和rm(1)命令是基于unlink函数实现的。

rename

rename(2)函数改变文件名,需要修改目录数据块中的文件名记录,如果原文件名和新文件名不在一个目录下则需要从原目录数据块中清除一条记录然后添加到新目录的数据块中。 mv(1)命令是基于rename函数实现的,因此在同一分区的不同目录中移动文件并不需要复制和删除文件的inode和数据块,只需要一个改名操作,即使要移动整个目录,这个目录下有很多子目录和文件也要随着一起移动,移动操作也只是对顶级目录的改名操作,很快就能完成。但是,如果在不同的分区之间移动文件就必须复制和删除inode和数据块,如果要移动整个目录,所有子目录和文件都要复制删除,这就很慢了。

mkdir/rmdir/opendir/readdir/closedir

mkdir(2)函数创建新的目录,要做的操作是在它的父目录数据块中添加一条记录,然后分配新的inode和数据块,inode的st_mode字段的文件类型是目录,在数据块中填两个记录,分别是.和..,由于..表示父目录,因此父目录的硬链接数要加1。 mkdir(1)命令是基于mkdir函数实现的。

rmdir(2)函数删除一个目录,这个目录必须是空的(只包含.和..)才能删除,要做的操作是释放它的inode和数据块,清除inode位图和块位图中相应的位,清除父目录数据块中的记录,父目录的硬链接数要减1。rmdir(1)命令是基于rmdir函数实现的。

opendir(3)/readdir(3)/closedir(3)用于遍历目录数据块中的记录。

  • opendir打开一个目录,返回一个DIR *指针代表这个目录,它是一个类似FILE *指针的句柄
  • closedir用于关闭这个句柄,把DIR *指针传给
  • readdir读取目录数据块中的记录,每次返回一个指向struct dirent的指针,反复读就可以遍历所有记录,所有记录遍历完之后readdir返回NULL。 结构体struct dirent的定义如下:
struct dirent {
	ino_t          d_ino;       /* inode number */
	off_t          d_off;       /* offset to the next dirent */
	unsigned short d_reclen;    /* length of this record */
	unsigned char  d_type;      /* type of file */
	char           d_name[256]; /* filename */
};

下面这个例子作用是递归地打印出一个目录下的所有子目录和文件,类似ls -R。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>

#define MAX_PATH 1024

/* dirwalk:  apply fcn to all files in dir */
void dirwalk(char *dir, void (*fcn)(char *))
{
	char name[MAX_PATH];
	struct dirent *dp;
	DIR *dfd;

	if ((dfd = opendir(dir)) == NULL) {
		fprintf(stderr, "dirwalk: can't open %s\n", dir);
		return;
	}
	while ((dp = readdir(dfd)) != NULL) {
		if (strcmp(dp->d_name, ".") == 0
		    || strcmp(dp->d_name, "..") == 0)
			continue;    /* skip self and parent */
		if (strlen(dir)+strlen(dp->d_name)+2 > sizeof(name))
			fprintf(stderr, "dirwalk: name %s %s too long\n",
				dir, dp->d_name);
		else {
			sprintf(name, "%s/%s", dir, dp->d_name);
			(*fcn)(name);
		}
	}
	closedir(dfd);
}

/* fsize:  print the size and name of file "name" */
void fsize(char *name)
{
	struct stat stbuf;

	if (stat(name, &stbuf) == -1) {
		fprintf(stderr, "fsize: can't access %s\n", name);
		return;
	}
	if ((stbuf.st_mode & S_IFMT) == S_IFDIR)
		dirwalk(name, fsize);
	printf("%8ld %s\n", stbuf.st_size, name);
}

int main(int argc, char **argv)
{
	if (argc == 1)  /* default: current directory */
		fsize(".");
	else
		while (--argc > 0)
			fsize(*++argv);
	return 0;
}

dup和dup2函数

#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);

 dup和dup2都可用来复制一个现存的文件描述符,使两个文件描述符指向同一个file结构体。  如果两个文件描述符指向同一个file结构体,File Status Flag和读写位置只保存一份在file结构体中,并且file结构体的引用计数是2。  如果两次open同一文件得到两个文件描述符,则每个描述符对应一个不同的file结构体,可以有不同的File Status Flag和读写位置。请注意区分这两种情况。  如果调用成功,这两个函数都返回新分配或指定的文件描述符,如果出错则返回-1。  dup返回的新文件描述符一定该进程未使用的最小文件描述符,这一点和open类似。  dup2可以用newfd参数指定新描述符的数值。如果newfd当前已经打开,则先将其关闭再做dup2操作,如果oldfd等于newfd,则dup2直接返回newfd而不用先关闭newfd再复制。

下面这个例子演示了dup和dup2函数的用法,请结合后面的连环画理解程序的执行过程。

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
	int fd, save_fd;
	char msg[] = "This is a test\n";

	fd = open("somefile", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
	if(fd<0) {
		perror("open");
		exit(1);
	}
	save_fd = dup(STDOUT_FILENO);
	dup2(fd, STDOUT_FILENO);
	close(fd);
	write(STDOUT_FILENO, msg, strlen(msg));
	dup2(save_fd, STDOUT_FILENO);
	write(STDOUT_FILENO, msg, strlen(msg));
	close(save_fd);
	return 0;
}

重点解释两个地方:

第3幅图,要执行dup2(fd, 1);,文件描述符1原本指向tty,现在要指向新的文件somefile,就把原来的关闭了,但是tty这个文件原本有两个引用计数,还有文件描述符save_fd也指向它,所以只是将引用计数减1,并不真的关闭文件。

第5幅图,要执行dup2(save_fd, 1);,文件描述符1原本指向somefile,现在要指向新的文件tty,就把原来的关闭了,somefile原本只有一个引用计数,所以这次减到0,是真的关闭了。