文件描述符
在内核中,所有打开的文件都使用文件描述符(一个非负整数)标记。文件描述符的变化范围是0~OPEN_MAX – 1。早期的unix系统中,每个进程最多可以同时打开20个文件,就是说文件描述符的范围为0~19,但是现在很多系统将其增加到0~63。
#include <fcntl.h> int open(const char* path, int oflag, ...); int openat(int fd, const char* path, int oflag, ...); 返回值:成功,文件描述符;失败,-1 |
path: 打开或创建文件的名字。 oflag: 用|连接的多个选项。 以下选项有且只能有一个: O_RDONLY(0)/O_WRONLY(1)/O_RDWR(2)/O_EXEC/O_SEARCH 以下选项可选: O_APPEND/O_CLOEXEC/O_CREAT/O_DIRECTORY(如果path不是目录,则出错)/O_EXCL(如果同时指定了O_CREATE,而文件已经存在,则出错)/O_NOCTTY/O_NOFOLLOW(如果path是一个符号链接,则出错)/O_NBLOCK(非阻塞模式)/O_SYNC(使得每次write等待物理I/O完成)/O_TRUNC(将文件截断为0)/O_TTY_INIT(如果打开一个还未打开的终端设备,设置非标准的termios参数值,使其符合Single UNIX Specification)/O_DSYNC(使得每次write等待物理I/O完成)/O_RSYNC(使每一个以文件描述符为参数的read操作等待,直至对文件的所有写操作完成) fd: 3种情况: path为绝对路径,此时fd被忽略,openat等价于open; path为相对路径,此时fd指定相对路径在文件系统中的开始地址,fd参数通过打开相对路径所在的目录来获取; path为相对路径,fd的值为AT_FDCWD,此时路径名在当前目录中获取; |
#include <unistd.h> int close(int fd); 返回值:成功,0;失败,-1 |
当一个进程终止时,内核自动关闭所有打开的文件,因此,很多时候不需要显式地调用close。 |
#include <unistd.h> off_t lseek(int fd, off_t offset, int whence); 返回值:成功,新的文件偏移量;失败,-1 |
文件偏移量为从文件开始处计算出的字节数,非负。默认情况下,打开一个文件时,偏移量被设为0(除非以O_APPEND模式打开)。 whence: 如果为SEEK_SET(0),则将偏移量设为距文件开始处offset个字节; 如果为SEEK_CUR(1),则将偏移量设为距文件当前处offset个字节(可正可负); 如果为SEEK_END(2),则将偏移量设为文件长度加offset个字节(可正可负); lseek仅将当前文件偏移量记录在内核中,用于下一次读或者写操作。文件偏移量可以大于当前文件长度,在这种情况下,对该文件的一次写操作将加长该文件,并在文件中形成一个空洞(属于文件但是没有写过的字节都被设为0),但是该空洞并不占用磁盘空间。 |
空洞程序示例: [root@benxintuzi IO]# cat hole.c #include <unistd.h> #include <fcntl.h> #include <stdio.h> char buf1[] = "benxintuzi"; char buf2[] = "BENXINTUZI"; int main(void) { int fd; if ((fd = open("file", O_RDWR | O_CREAT)) == -1) printf("create error\n"); if (write(fd, buf1, 10) != 10) printf("buf1 write error\n"); /* offset now = 10 */ if (lseek(fd, 16384, SEEK_SET) == -1) printf("lseek error\n"); /* offset now = 16384 */ if (write(fd, buf2, 10) != 10) printf("buf2 write error\n"); /* offset now = 16394 */ return 0; } [root@benxintuzi IO]# ls -l file --wsr-S---. 1 root root 16394 Aug 25 02:13 file [root@benxintuzi IO]# od -c file 0000000 b e n x i n t u z i \0 \0 \0 \0 \0 \0 0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 * 0040000 B E N X I N T U Z I 0040012 |
#include <unistd.h> ssize_t read(int fd, void* buf, size_t nbytes); 返回值:成功,读出的字节数;失败,-1;遇到文件尾,0 |
#include <unistd.h> ssize_t write(int fd, const void* buf, size_t nbytes); 返回值:成功,写入的字节数;失败,-1 |
#include <stdio.h> #include <unistd.h> #define BUFFSIZE 4096 int main(void) { int n; char buf[BUFFSIZE]; while((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) if(write(STDOUT_FILENO, buf, n) != n) printf("write error\n"); if(n < 0) printf("read error\n"); return 0; } [root@benxintuzi IO]# ./read benxintuzi benxintuzi hello hello tuzi tuzi ^C [root@benxintuzi IO]# |
文件共享
Unix支持在不同进程间共享打开的文件。Unix内核使用3种数据结构表示打开的文件:
注:创建v节点的目的是为了支持一个计算机系统上的多种文件系统类型。Linux系统没有使用v节点,而是使用了通用的i节点结构。
两个独立进程打开同一文件如下:
第一个进程在文件描述符3上打开该文件,第二个进程在文件描述符4上打开该文件。每个进程都获得独立的文件表项,但却指向同一个v节点。
#include <unistd.h> int dup(int fd); int dup2(int fd, int fd2); 函数返回新的文件描述符,与fd共享同一个文件表项。 说明: 由dup返回的文件描述符一定是当前可用的最小的文件描述符。对于dup2,可以用fd2指定特定的新的文件描述符。如果fd2已经打开,则先将其关闭;如果fd == fd2,则不关闭而直接返回。 |
![]() |
复制一个文件描述符的另一种方式是使用fcntl函数:
dup(fd)
等价于:
fcntl(fd, F_DUPFD, 0);
dup2(fd, fd2);
等价于:
close(fd2);
fcntl(fd, F_DUPFD, fd2);
#include <unistd.h> int fsync(int fd); int fdatasync(int fd); void sync(void); 返回值:成功,0;失败,-1 说明: sync:将修改过的缓冲区数据块放到写队列中,立即返回,而不等待实际写磁盘结束。 fsync:只对fd指定的文件块起作用,并且等待写磁盘结束才返回,同时更新文件的属性部分。 fdatasync:与fsync等价,只是其只关注数据部分,不更新文件属性部分。 |
|
#include <fcntl.h> int fcntl(int fd, int cmd, ...); 说明: fcntl函数的5种功能: (1) 复制一个已有的文件描述符(cmd = F_DUPFD或F_DUPFD_CLOEXEC) (2) 获取/设置文件描述符标志(cmd = F_GETFD或F_SETFD) (3) 获取/设置文件状态标志(cmd = F_GETFL或F_SETFL) (4) 获取/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN) (5) 获取/设置记录锁(cmd = F_GETLK/F_SETLK/F_SETLKW) |
/dev/fd目录下是名为0、1、2等的文件,打开文件/dev/fd/n等价于复制文件描述符n,因此, fd = open(“/dev/fd/0”, mode);等价于fd = dup(0); |
文件结构
#include <sys/stat.h> int stat(const char* restrict pathname, struct stat* restrict buf); int fstat(int fd, struct stat* buf); int lstat(const char* restrict pathname, struct stat* restrict buf); int fstatat(int fd, const char* restrict pathname, struct stat* restrict buf, int flag); 返回值:成功,0;失败,-1 stat函数系列返回相关的结构体信息。 fstat返回与fd相关的文件信息/lstat返回符号链接相关的信息/... buf参数是一个结构体指针,由stat系列函数负责填充buf结构体。 |
struct stat { mode_t st_mode; /* file type & mode(permissions) */ ino_t st_ino; /* i-node number(serial number) */ dev_t st_dev; /* device number(file system) */ dev_t st_rdev; /* device number for special files */ nlink_t st_nlink; /* numbers of links */ uid_t st_uid; /* user ID of owner */ gid_t st_gid; /* group ID of owner */ off_t st_size; /* size in bytes, for regular files */ struct timespec st_atime; /* time of last access */ struct timespec st_mtime; /* time of last modification */ struct timespec st_ctime; /* time of last file status change */ blksize_t st_blksize; /* best I/O block size */ blkcnt_t st_blocks; /* number of disk blocks allocated */ } |
Unix文件类型
(1) 普通文件
(2) 目录文件:包含其他文件的名字以及指向这些文件的指针。
(3) 块特殊文件:提供对设备带缓冲的访问,每次访问以固定长度为单位进行。
(4) 字符特殊文件:每次访问长度单位可变(系统中的文件要么是字符特殊文件,要么是块特殊文件)。
(5) FIFO:用于进程间通信。
(6) 套接字:用于进程间的网络通信,也可用于一台主机上的进程之间的非网络通信。
(7) 符号链接:指向另一文件。
可以用如下宏确定文件类型:
宏 | 文件类型 |
S_ISREG() | 普通文件 |
S_ISDIR() | 目录文件 |
S_ISBLK() | 块特殊文件 |
S_ISCHR() | 字符特殊文件 |
S_ISFIFO() | FIFO |
S_ISSOCK() | 套接字 |
S_ISLNK() | 符号链接 |
[root@benxintuzi IO]# cat types.c #include <sys/stat.h> #include <stdio.h> int main(int argc, char** argv) { int i; struct stat buf; char* ptr; for(i = 1; i < argc; i++) { printf("%s: ", argv[i]); if(lstat(argv[i], &buf) < 0) { printf("lstat error\n"); continue; } if(S_ISREG(buf.st_mode)) ptr = "regular"; else if(S_ISDIR(buf.st_mode)) ptr = "directory"; else if(S_ISBLK(buf.st_mode)) ptr = "block special"; else if(S_ISCHR(buf.st_mode)) ptr = "character special"; else if(S_ISFIFO(buf.st_mode)) ptr = "fifo"; else if(S_ISLNK(buf.st_mode)) ptr = "symbolic link"; else if(S_ISSOCK(buf.st_mode)) ptr = "socket"; else ptr = "unknown mode"; printf("%s\n", ptr); } return 0; } [root@benxintuzi IO]# gcc types.c -o types [root@benxintuzi IO]# ./types /etc/passwd /etc /dev/tty /dev/cdrom /etc/passwd: regular /etc: directory /dev/tty: character special /dev/cdrom: symbolic link
文件访问权限
用户ID和组ID:
实际用户ID 实际组ID | 登录时取自口令文件中的登录项,表明是谁在登录系统 |
有效用户ID 有效组ID 附属组ID | 表明了对文件的访问权限 |
设置用户ID 设置组ID | 保存了有效用户ID和有效组ID的副本 |
通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID |
所有类型的文件都有访问权限设定,可以分为三类,共有9个权限位:
st_mode屏蔽 | 说明 |
S_IRUSR S_IWUSR S_IXUSR | 用户读 用户写 用户执行 |
S_IRGRP S_IWGRP S_IXGRP | 组读 组写 组执行 |
S_IROTH S_IWOTH S_IXOTH | 其他读 其他写 其他执行 |
进程每次打开、创建或者删除一个文件时,内核会进行文件访问权限测试,具体如下(顺序执行):
(1) 若进程的有效用户ID是0(超级用户),则允许访问;
(2) 若进程的有效用户ID等于文件的所有者ID,则根据所有者的权限置位情况开放给用户相应权限;
(3) 若进程的有效组ID等于文件的组ID,则根据组的权限置位情况开放相应权限;
(4) 若其他用户的适当权限位被设置,则开放相应权限。
#include <unistd.h> int access(const char* pathname, int mode); int faccessat(int fd, const char* pathname, int mode, int flag); 返回值:成功,0;失败,-1 说明: access函数对按实际用户ID和实际组ID进行访问权限测试。 关于mode,如果测试文件存在,则mode设为F_OK;否则设为R_OK/W_OK/X_OK的按位或。 关于flag,如果flag设置为AT_EACCESS,则按有效用户ID和有效组ID进行测试。 |
[root@benxintuzi IO]# cat access.c #include <fcntl.h> #include <stdio.h> int main(int argc, char** argv) { if(argc != 2) { printf("usage: execfile <pathname>\n"); return 1; } if(access(argv[1], R_OK) < 0) printf("access error for %s\n", argv[1]); else printf("read access OK\n"); return 0; } [root@benxintuzi IO]# gcc access.c -o access [root@benxintuzi IO]# ./access usage: execfile <pathname> [root@benxintuzi IO]# ./access types.c read access OK [root@benxintuzi IO]# ./access types read access OK [root@benxintuzi IO]# ./access / read access OK
#include <sys/stat.h> mode_t umask(mode_t cmask); 说明: 在进程创建一个新文件或新目录时,必须使用文件模式屏蔽字,其指定了新文件的访问权限位。屏蔽字中为1表示模式中的相应位被关闭。Unix系统的大多数用户从不处理umask值。尽管如此,当创建新文件时,如果我们想确保指定的访问权限已经激活,那么必须在进程运行时修改umask值。 |
#include <fcntl.h> #include <stdio.h> #define RWRWRW (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) int main(void) { umask(0); # 开放全部权限 if(creat("foo", RWRWRW) < 0) printf("create error for foo\n"); umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); # 关闭组和其他用户的访问权限 if(creat("bar", RWRWRW) < 0) printf("create error for bar\n"); return 0; } [root@benxintuzi IO]# ./umask [root@benxintuzi IO]# ls -l foo bar -rw-------. 1 root root 0 Aug 25 21:57 bar -rw-rw-rw-. 1 root root 0 Aug 25 21:57 foo
#include <sys/stat.h> int chmod(const char* pathname, mode_t mode); int fchmod(int fd, mode_t mode); int fchmodat(int fd, const char* pathname, mode_t mode, int flag); 返回值:成功,0;失败,-1 说明: 函数用于改变现有文件的访问权限。 chmod对指定文件进行操作,fchmod/fchmodat对已打开的文件进行操作。 mode是如下常量的按位或:
|
[root@benxintuzi IO]# cat chmod.c #include <sys/stat.h> #include <stdio.h> int main(void) { struct stat statbuf; /* turn on set-group-ID and turn off group-execute */ if(stat("foo", &statbuf) < 0) printf("stat error for foo\n"); if(chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0) printf("chmod error for foo"); /* set absolute mode to "rw-r--r--" */ if(chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0) printf("chmod error for bar"); return 0; } [root@benxintuzi IO]# gcc chmod.c -o chmod [root@benxintuzi IO]# ./chmod [root@benxintuzi IO]# ls -l foo bar -rw-r--r--. 1 root root 0 Aug 25 21:57 bar -rw-rwSrw-. 1 root root 0 Aug 25 21:57 foo
#include <unistd.h> int chown(const char* pathname, uid_t owner, gid_t group); int fchown(int fd, uid_t owner, gid_t group); int fchownat(int fd, const char* pathname, uid_t owner, gid_t group, int flag); int lchown(const char* pathname, uid_t owner, gid_t group); 返回值:成功,0;失败,-1 chown函数用于改变文件的用户ID和组ID。 |
#include <unistd.h> int truncate(const char* pathname, off_t length); int ftruncate(int fd, off_t length); 返回值:成功,0;失败,-1 将文件截断为length个字节。如果文件当前大于length,则截断;如果小于length,则增加。增加的部分数据为0(就是一个空洞)。 |
文件系统
#include <unistd.h> int link(const char* existingpath, const char* newpath); int linkat(int efd, const char* existingpath, int nfd, const char* newpath, int flag); 返回值:成功,0;失败,-1 说明: 创建一个指向现有文件的链接。 引用现有文件existingpath创建新的newpath,实则是增加一个链接计数。 |
#include <unistd.h> int unlink(const char* pathname); int unlinkat(int fd, const char* pathname, int flag); 返回值:成功,0;失败,-1 说明: 删除目录项,并将pathname所引用的文件的链接数减1。如果链接计数减少为0,则删除该文件。 |
#include <stdio.h> int rename(const char* oldname, const char* newname); int renameat(int oldfd, const char* oldname, int newfd, const char* newname); 返回值:成功,0;失败,-1 |
符号链接
硬链接是直接指向文件的i节点,通常要求链接和文件位于同一文件系统中。只有超级用户才能创建指向目录的硬链接。
相对地,符号链接是对一个文件的间接指针,符号链接指向的对象没有特殊限制,任何用户都可以创建指向目录的符号链接。其一般用户将一个文件或目录结构移到系统中的另一个位置。
#include <unistd.h> int symlink(const char* actualpath, const char* sympath); int symlinkat(const char* actualpath, int fd, const char* sympath); 返回值:成功,0;失败,-1 说明: 创建符号链接。actualpath和sympath可以不位于同一文件系统中。 |
#include <unistd.h> ssize_t readlink(const char* restrict pathname, char* restrict buf, size_t bufsize); ssize_t readlinkat(int fd, const char* restrict pathname, char* restrict buf, size_t bufsize); 返回值:成功,读到的字节数;失败,-1 说明: 由于open函数打开符号链接时,是打开链接指向的实际文件,因此需要readlink打开符号链接本身,将链接本身信息存入buf中。 |
关于时间
每个文件需要维护3个时间字段:
字段 | 说明 |
st_atim | 文件的最后访问时间 |
st_mtim | 文件的最后修改时间 |
st_ctim | i节点的最后更改时间 |
#include <sys/stat.h> int futimens(int fd, const struct timespec times[2]); int utimensat(int fd, const char* path, const struct timespec times[2], int flag); 返回值:成功,0;失败,-1 说明: 函数用于更改一个文件的访问和修改时间,可以精确到纳秒级。 times数组的第一个元素是访问时间,第二个是修改时间,有4种指定方式: (1) 如果times参数是一个空指针,则访问时间和修改时间都设置为当前时间; (2) 如果times参数指向两个timespec结构,并且若timespec结构的tv_nsec字段为UTIME_NOW,则相应时间设为当前时间,忽略对应的tv_sec; (3) 如果times参数指向两个timespec结构,并且若timespec结构的tv_nsec字段为UTIME_OMIT,则相应时间保持不变,忽略对应的tv_sec; (4) 如果times参数指向两个timespec结构,并且若timespec结构的tv_nsec字段既非UTIME_NOW,也非UTIME_OMIT,则相应时间设为tv_sec和tv_nsec。 |
关于目录
#include <sys/stat.h> int mkdir(const char* pathname, mode_t mode); int mkdirat(int fd, const char* pathname, mode_t mode);
#include <unistd.h> int rmdir(const char* pathname);
#include <dirent.h> DIR* opendir(const char* pathname); DIR* fdopendir(int fd); struct dirent* readdir(DIR* dp); void rewinddir(DIR* dp); int closedir(DIR* dp); long telldir(DIR* dp); void seekdir(DIR* dp, long loc);
#include <unistd.h> int chdir(const char* pathname); int fchdir(int fd); 说明: 更改当前进程的工作目录,不影响其他进程。
#include <unistd.h> char* getcwd(char* buf, size_t size); 返回当前工作目录的绝对路径,存入buf中。 |
设备特殊文件
每个文件系统所在的存储设备都由其主、次设备号表示,数据类型为dev_t。主设备号标识驱动程序,此设备号标识特定的子设备。因此,在同一磁盘驱动器上的各个文件系统通常具有相同的主设备号,但是次设备号却不同。宏major和minor用来访问主、次设备号,Linux将其定义在<sys/sysmacros.h>中,而该头文件又包含在<sys/types.h>中。
#include <sys/types.h> #include <sys/stat.h> #include <stdio.h> int main(int argc, char** argv) { int i; struct stat buf; for(i = 1; i < argc; i++) { printf("%s: ", argv[i]); if(stat(argv[i], &buf) < 0) { printf("stat error\n"); continue; } printf("dev = %d/%d ", major(buf.st_dev), minor(buf.st_dev)); if(S_ISCHR(buf.st_mode) || S_ISBLK(buf.st_mode)) { printf("rdev = %d/%d", major(buf.st_rdev), minor(buf.st_rdev)); } printf("\n"); } return 0; } [root@benxintuzi IO]# ./dev / /home /dev/tty[01] /: dev = 8/2 /home: dev = 8/2 /dev/tty0: dev = 0/5 rdev = 4/0 /dev/tty1: dev = 0/5 rdev = 4/1