Linux文件系统及相关操作函数
文件系统
文件系统的基本概念
1.在Unix操作系统中,每个文件以一个或多个数据块离散的存放在磁盘的块表区中(柱面表区);
2.一个文件的每个数据块的存储位置保存在I节点(i-node)中;
3.对应系统中多个文件的I节点保存在磁盘的I节点表中,每个I节点通过一个下标唯一地标识,这个下标称为I节点号;
4.文件的名称和其所对应的I节点号保存在目录文件中;
5.访问文件的过程:根据文件名到目录文件中找到相应的I节点号,再根据I节点号到I节点表中找到对应的I节点,根据I节点找到每个数据块在磁盘上的存储位置,进而访问其中的数据;
6.根目录/的I节点信息保存在一个特殊的位置即超级块super block中,每次挂载(mount)文件系统时首先找到根目录的位置;
7.以上所述文件数据的存储结构和访问方式,被称为文件系统fs;
格式化硬盘的过程实际就是重新建立文件系统的过程;
一切皆文件
fscanf(stdin, ...);
fprintf(stdout, ...);
Linux环境下文件具有特殊的意义,它为系统中的服务和设备提供了一种抽象接口;
程序完全可以向访问普通文件一样,访问串口/网络/鼠标/键盘/打印机等各种设备;
大多数情况下只需要使用五个最基本的系统调用
open()/close()/read()/write()/ioctl();
广义文件
1-> 目录文件
2-> 设备文件
控制台 /dev/console
声卡 /dev/audio
标准输入输出 /dev/tty
空设备 /dev/null
文件相关的系统调用
open() //用来打开/创建文件,类似于fopen();
close() //用来关闭文件,类似于fclose();
read() //用来读取文件,类似于fread();
write() //用来写入文件,类似于fwrite();
lseek() //设置读写位置,类似于fseek()/ftell();
creat() //创建文件;
fcntl() //修改文件属性;
unlink() //删除硬链接文件;
rmdir() //删除空目录
remove() //删除硬链接(unlink)/删除空目录(rmdir);
注意
1.如果被unlink/remove删除的是文件的最后一个硬链接,并且没有进程正在打开该文件,那么该文件在磁盘上的存储区域将被立即标记为free;
反之,如果有进程正在打开该文件,那么该文件在目录中条目虽然被立即删除,但它在磁盘上的存储区域将在所有打开它的进程关闭该文件以后才会被标记为free;
2.如果被unlink/remove删除的是一个软链接文件,那么仅软链接本身被删除,源文件不受影响;
创建硬链接相当于创建了一个别名,还是统一块数据区域,可以用ls -i命令查看,inode节点号是一样的;
文件描述符
int fd = open(...);
1.文件描述符的本质就是内核为每个进程维护的文件描述符表中存储的文件表指针的下标;//深刻理解
文件描述符表(在内核层)中存储的文件表指针指向"文件表";
文件表中存储着文件的状态和位置信息以及v-node指针;
v-node指针指向v-node表(内核层);
v-node表中存放着磁盘中i-node表的拷贝;
访问文件时会根据I节点号到I节点表中找到对应的I节点,根据I节点找到每个数据块在磁盘上的存储位置,进而访问其中的数据;
+----+ +------------+ +------+ +--------+ +------------+
| fd |->|文件描述符表| |文件表| |v-node表| |磁盘i-node表|
| | |以文件描述符| | 状态 | | | | |
| | |为下标的文件| | 位置 | | inode | | |
| | |表指针 |->|v-node| | 的拷贝 |->| |
| | | | |指针 |->| | | |
+----+ +------------+ +------+ +--------+ +------------+
grep __IO_FILE /usr/include/*.h
/usr/include/stdio.h:typeded struct __IO_FILE FILE;
grep _fileno /usr/include/*.h -n
/usr/include/libio.h:294: int _fileno;
vi /usr/include/libio.h +294
在标准C语言中FILE *fp = fopen();
_fileno是FILE的其中一个成员,而_fileno就是文件描述符;
2.文件描述符是一个非负的整数;
3.每个文件描述符对应一个打开的文件;
4.文件描述符从open函数返回,同时作为参数传递给后续的文件函数(read/write/close...)使用;
5.内核缺省为每个进程打开三个文件表述符:
标准输入0 STDIN_FILENO
标准输出1 STDOUT_FILENO
标准错误输出2 STDERR_FILENO
#include <unistd.h>
6.可以在输入输出重定向时指定定向文件描述符;
7.一个进程可以使用的文件描述符的范围是0到OPEN_MAX;OPEN_MAX宏在早期的Unix/Linux中被定义为63,所以一个进程同时最多只能打开64个文件;现在的Unix/Linux系统可以同时打开更多文件;
通过sysconf(_SC_OPEN_MAX)调用可以获取当前系统所允许的进程最多同时打开的文件描述符个数;
Unix系统文件相关的系统调用
open()/creat()/close()
对应的头文件是#include <fcntl.h>
int open(
const char *pathname, //文件路径;
int flags, //状态标志;
mode_t mode //权限模式,只在创建时有效;
); //打开/创建文件都可以使用此函数;
flags为以下值的位或
O_RDONLY //只读|
O_WRONLY //只写|->三个只能选一个;
O_RDWR //读写|
O_APPEND //追加,注意追加不可以和只读放在一起,否则冲突;
O_CREAT //创建,不存在即创建;已存在,直接打开,保留原有内容;
//除非位或了下面的标志...;
O_EXCL //排斥exclude,已存在则失败,防止意外覆盖原有文件;
//可以用errno来判断失败的原因;
O_TRUNC //清空,已存在则清空;
//O_EXCL/O_TRUNC只能选一个与0_CREAT位或;
mode的取值与chmod命令的权限参数一致,均为八进制;
open()成功则返回文件描述符,失败则返回-1;
int creat(
const char *pathname, //文件路径;
mode_t mode //权限模式
//仅创建有效
); //常用于创建文件
成功返回文件描述符,失败返回-1;相当于这样调用open函数:
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
关闭文件的实质是释放内核中的文件描述符表;
int close(int fd); //成功返回0,失败返回-1;
实际创建文件的权限 = mode & ~umask;
因此所有umask(权限掩码)中包含的权限,在创建的文件中都不会存在;
如:0666 & ~0002 == 0664;
write()
#include <unistd.h>
ssize_t write(
int fd, //文件描述符;
const void *buf, //操作地址;从buf写入到fd对应文件;
size_t count //期望写入字节数;
); //成功时返回实际写入字节数,失败则返回-1;
ssize_t //int
size_t //尽量使用这个类型,会随系统而变;
32位中为 unsigned int
64位中为 unsigned long int
read()
#include <unistd.h>
ssize_t read(
int fd, //文件描述符
void *buf, //操作缓冲区;//从fd对应文件读取到buf中;
size_t count //期望读取的字节数;
); //成功时返回实际读取字节数;
读到文件尾返回0,如果读取失败返回-1;
标C中有一个函数feof();可以判断是否读到文件尾,如果是则返回1;
练习
写一个文件拷贝程序,带有覆盖检查功能;
提示
在一个循环中,读一块,写一块,读完结束;
调用open函数打开目标文件时,给O_WRONLY|O_CREAT|O_EXCL,若目标文件存在,则open失败,判断errno == EEXIST,说明目标已经存在;
练习
分别以二进制和纯文本方式读写文件;
二进制文件与纯文本文件取决于文件的存储方式,一个是存储数据本身,一个是存储数据格式化成文本之后的数据;
hexdump -C txt.txt 命令可以以16进制查看文件
在标准C语言中可以直接调用fprintf()函数以文本方式写入文件;
使用fgets()一次读取文件的一行;
使用feof()函数判断是否已经读到文件末尾;
使用以下命令编译和测试
gcc stdio.c -o stdio
gcc sysio.c -o sysio
time sysio
time stdio
发现stdio明显的比较快;
lseek()
每个打开的文件都有一个与之相关的"文件位置";
文件位置通常是一个非负的整数,用于度量从文件头开始计算的字节数;
所有的读写操作都从当前文件位置开始,并根据所读写的字节数增加文件位置;
打开一个文件时,除非指定了O_APPEND,否则文件位置一律被设为0;
lseek()函数仅修改文件表中记录的文件位置,并不引发任何IO动作;
lseek()函数可以将文件位置设置到文件尾之后,这样在后续写操作以后会在文件中形成空洞;文件空洞不占用磁盘空间,但会被计算在文件大小内;//下载软件在下载文件时候会首先建立相应大小的空文件;
#include <sys/types.h>
#include <unistd.h>
off_t lseek(
int fd, //文件描述符;
off_t offset; //偏移量;
int whence //起始位置;
); //成功返回调整以后的文件位置,失败返回-1;
whence取值
SEEK_SET //文件头(文件的第一个位置);
SEEK_CUR //从当前位置(下一次读写操作的位置);
SEEK_END //从文件尾(文件最后一个字节的下一个位置);
dup()/dup2()函数
duplicate a file descripter;获得文件描述符的副本;
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
成功时返回文件描述符oldfd的副本(新的描述符),失败返回-1;
复制一个已打开文件的描述符;
dup()函数返回的一定是当前未使用的最小的文件描述符;
dup2()函数可以由第二个参数newfd指定期望复制到的文件描述符,若所指定的文件描述符已打开,则先关闭之,再复制;
所返回的文件描述符副本与源文件描述符oldfd对应同一个文件表;
文件描述符与文件并不是一一对应;
注意区分通过dup()/dup2()获得文件描述符副本,和两次open()同一个文件的区别
dup()/dup2()只复制文件描述符表中的文件表指针,不会重建新的文件表;
fd1 |->文件表->V节点(I节点信息表);
fd2 |
没有创建新的文件表;操作fd1和fd2会同步操作同一个文件;
open()会创建一个新的文件表,并为其分配新的文件描述符:
fd2->文件表 |->v节点(I节点信息表);
fd1->文件表 |
多次open()同一文件,同时操作fd1和fd2会相互覆盖文件内容
其实shell中的输出重定向操作就是通过这种方式来操作的;
sync()/fsync()/fdatasync()
fflush()
大多数的磁盘IO都要通过缓冲进行,写入文件其实只是写入缓冲区,直到缓冲区满,才将其排入队列;
延迟写降低了写操作的次数;提高写操作的效率,但是会导致磁盘文件与缓冲区数据不同步;
sync()/fsync()/fdatasync()用于强制磁盘文件与缓冲区同步;
sync()将把进程中所有被修改过的缓冲区排入写队列,立即返回,并不等待写磁盘操作完成; //不带参数
fsync()只针对一个文件,且一直等到写磁盘操作完成才返回;
fdatasync()只同步文件数据,不同步文件属性;
#include <unistd.h>
void sync(void);
int fsync(int fd);
int fdatasync(int fd);/成功返回0,失败返回-1;
应用程序内存
| |
fwrite() |
| |
V |
标准库缓冲 write()
| |
fflush() |
V V
内 核 缓 冲 区
|
sync()/fsync()/fdatasync()
|
V
写队列/磁盘设备
fcntl()
操作文件描述符
#include <unistd.h>
#include <fcntl.h>
int fcntl(
int fd, //文件描述符
int cmd, //控制指令
... //可变参数,因控制指令而异
); //成功的返回值因cmd而异,失败返回-1;
对fd所表示的文件执行cmd所表示的控制,某些控制需要提供参数,某些控制会返回特定的值;
1.复制文件描述符
int fd = fcntl(oldfd, F_DUPDF); //返回新的文件描述符
int fd = fcntl(oldfd, F_DUPFD, newfd); //返回新的文件描述符
复制oldfd为不小于newfd的文件描述符;
若newfd文件描述符已用,该函数就会选择比newfd大的最小未用值,而非dup2()函数那样关闭之;
2.获取/设置文件状态标志
一个状态标志占一个位;
int flags = fcntl(fd, F_GETFL); //get flag
if (flags & O_RDONLY) {
/* 位操作,flags中O_RDONLY位为1 */
} else {
/* flags中O_RDONLY位为0 */
}
不能获取O_CREAT|O_EXCL|O_TRUNC与创建有关的属性;
能访问就说明文件已经存在了,跟创建相关的已经没有必要获取;
int res = fcntl(fd, F_SETFL, flags); //追加flags标志;
在现有标志的基础上追加flags标志;
但只能追加O_APPEND|O_NONBLOCK属性;
3.给文件加锁
假设有一个文件同时被两个进程访问该文件的A区和B区,但是A区和B区有重叠;
第1种情况 在写锁区上加写锁;
-----------------------------------------------------------
进程1 进程2
打开文件,准备写A区; 打开文件,准备写B区;
调用fcntl给A区加写锁
fcntl返回成功,A区被
加上写锁;
调用write写A区; 调用fcntl给B区加写锁,fcntl阻塞
或返回失败;
调用fcntl,解锁A区; fcntl返回成功,B区被加上写锁;
调用write,写B区;
调用fcntl解锁B区;
关闭文件; 关闭文件;
===========================================================
第2种情况 在写锁区上加读锁
------------------------------------------------------------
进程1 进程2
打开文件,准备写A区 打开文件,准备读B区
调用fcntl给A区加写锁
fcntl返回成功,A区被
加上写锁;
调用write写A区, 调用fcntl给B区加读锁,fcntl阻塞
或返回失败;
调用fcntl,解锁A区; fcntl返回成功,B区被加上读锁;
调用read,读B区;
调用fcntl解锁B区;
关闭文件; 关闭文件;
===========================================================
第3种情况 在读锁区上加写锁
----------------------------------------------------------
进程1 进程2
打开文件,准备读A区 打开文件,准备写B区
调用fcntl给A区加读锁
fcntl返回成功,A区被
加上读锁;
调用read读A区; 调用fcntl给B区加读锁,fcntl阻塞
或返回失败;
调用fcntl,解锁A区; fcntl返回成功,B区被加上写锁;
调用write,写B区;
调用fcntl解锁B区;
关闭文件; 关闭文件;
===========================================================
第4种情况 在读锁区上加读锁
------------------------------------------------
进程1 进程2
打开文件,准备读A区; 打开文件,准备读B区;
调用fcntl给A区加读锁
fcntl返回成功,A区被
加上读锁;
调用read读A区; 调用fcntl给B区加读锁,
fcntl返回成功;
B区被加上读锁;
调用fcntl,解锁A区; 掉用read,读B区;
调用fcntl解锁B区;
关闭文件; 关闭文件;
===========================================================
试图在区域上加
读锁 写锁
区域上没有任何锁 OK OK
区域上已经有读锁 OK NO
区域上已经有写锁 NO NO
读锁也称为共享锁,写锁也称为独占锁(或排他锁);
无论是读锁还是写锁都是协议锁(或君子锁,劝谏锁);
如果有人不检查直接就write()也是没用的;
int fcntl(int fd,
F_SETLKW/*会等待*//F_SETLK/*不等待*/,
struct flock *lock
);
struct flock{
short int l_type;//锁类型;
//F_RDLCK-读锁
//F_WRLCK-写锁
//F_UNLCK-解锁
short int l_whence;//锁区偏移起点;
//SEEK_SET-文件头
//SEEK_CUR-当前位
//SEEK_END-文件尾
off_t l_start;//锁区偏移;
//从l_whence开始计算;
off_t l_len;//锁区长度;
//0表示锁到文件尾;
pid_t l_pid;//进程加锁,若加锁给-1;
};
例如
1->给文件从距离文件头10个字节处锁到第40个字节处,加写锁;
struct flock lck;
lck.l_type = F_WRLCK; //F_UNLCK时是解锁;fcntl中不需要更改参数;
lck.l_whence = SEEK_SET;
lck.l_start = 10;
lck.l_len = 30;
lck.l_pid = -1;
fcntl(fd, F_SETLKW, &lck);
如果成功返回0,如果失败返回-1;
2->给文件解锁;
struct flock lck;
lck.l_type = F_UNLCK; //解锁
lck.l_whence = SEEK_SET;
lck.l_start = 10;
lck.l_len = 30;
lck.l_pid = -1;
fcntl(fd, F_SETLKW, &lck);
以F_SETLK方式加锁,fcntl不会阻塞;
如果加锁失败返回-1,此时的errno == EACCES/EAGAIN;
如果加了锁但是没解锁,如果进程结束了或文件描述符被关闭了,进程里边跟该文件相关的协议锁自然就消失了;
文件锁标志是保存在v-node中的,并不是保存在文件表中,因为不同的进程open()同一个文件的时候,操作的是不同的文件表,但却都对应一个v-node;当加锁或解锁的时候都会检查v-node,并在加锁时添加到某个链表里;如果进程结束,文件表也就消失了,内核也会删掉对应进程的文件锁;
在v-node中一个文件可能会被不同的进程打开并加锁,加锁将按进程信息保存在某个链表中,进程操作时就会去检查锁的标志;如果某个进程没有锁协定,就不受锁的限制;
文件写锁测试,编译后在不同的终端同时写,后一个进程将等待第一个进程解锁后才能继续写操作;
文件读锁测试,编译后在不同的终端一个写,一个读,读操作将等待写操作解锁后才能继续读操作;
stat()/fstat()/lstat()
获取文件属性(元数据);
#include <sys/stat.h>
int stat(
const char *path; //文件路径;
struct stat *buf; //文件属性;
);
int fstat(
int fd, //文件描述符;
struct stat *buf; //文件属性;
);
int lstat(
const char *path; //文件路径;
struct stat *buf; //文件属性;
); //不跟踪软链接
第一个参数叫输入参数,第二个参数较输出参数;
返回值0表示成功,-1表示失败;执行结果由输出参数获取;
fstat()函数需要首先打开文件,传入文件描述符;其他两个函数不需要打开;
当用stat()操作一个软链接文件,获取的是软链接目标文件的属性;
而用lstat()操作一个软链接文件,获取的是软链接本身的属性;
struct stat {
dev_t st_dev; //设备ID;
ino_t st_ino; //inode;
mode_t st_mode; //权限和类型;
nlink st_nlink; //硬链接数;
uid_t st_uid; //属主ID;
gid_t st_gid; //属组ID;
dev_t st_rdev; //特殊设备ID;
off_t st_size; //总字节数;
blksize_t st_blksize; //I/O块字节数;
blkcnt_t st_blocks; //占用块(512B)数;windows叫簇,linux叫块
time_t st_atime; //最后访问时间;
time_t st_mtime; //最后修改时间;
time_t st_ctime; //最后状态改变时间;
};
其中st_mode(0TTSUGO) //第一个为数字零表示八进制;后一个是大写字母o;
TT //文件的类型; 6bit
S_IFDIR //目录类型(d); directory
S_IFREG //普通文件类型(-); regular
S_IFLNK //软链接(l); link
S_IFBLK //块设备(b); block
S_IFCHR //字符设备(c); character
S_IFSOCK //Unix域套接字(s); socket
S_IFIFO //有名管道(p); first in fist out
S //附加模式(可执行位); 3bit
S_ISUID //设置用户ID位(属主可执行位x -> s/S);
S_ISGID //设置组ID位(属组可执行位x -> s/S);
S_ISVTX //粘滞位(其他用户可执行位x -> t/T);
//-rwsr-sr-x
//drwxrwxrwt (如/tmp)
U //属主的权限; 3bit
S_IRUSR //属主可读
S_IWUSR //属主可写
S_IXUSR //属主可执行
G //属组的权限; 3bit
S_IRGRP //属组可读
S_IWGRP //属组可写
S_IXGRP //属组可执行
O //其他用户的权限; 3bit
S_IROTH //其他用户可读
S_IWOTH //其他用户可写
S_TXOTH //其他用户可执行
1.任何用户登录系统,都会启动一个shell进程,该进程的属主和属组取决于登录用户的UID和GID;
2.由登录shell启动的任何进程,其属主和属组都继承自该shell进程,此UID和GID被称为实际UID和实际GID;
3.决定一个进程对系统的访问能力的是有效UID和有效GID,一般情况下进程的有效UID和有效GID与其实际UID和实际GID一样;
4.如果一个可执行程序带有设置用户ID或者设置组ID位,那么运行该程序的进程的有效UID和有效GID就取自"该程序的属主和属组";
ls -l /usr/bin/passwd
-rwsr-xr-x. 1 root root 25980 2月 22 2012 /usr/bin/passwd
该文件只有root才可以修改,但是有s属性,普通用户也可以运行该命令修改自己的密码;
5.所谓粘滞位,就是具有该位的目录下面的文件只能被该文件的属主或者是root删除或更名;
粘滞位只能指针目录设置,对于文件无效;设置了粘滞位权限的目录,使用ls命令查看其属性时,其他用户权限处的x将变成t;使用chmod命令设置目录权限时,o+t,o-t权限模式可分别用于添加/移除粘滞位权限;
当目录被设置了粘滞位权限以后,即便用户对该目录有写权限,也不能删除该目录下的其他用户的文件数据,而只能有该文件的所有者和root用户才能有权删除;
对目录设置粘滞位以后,允许各用户在目录中任意写入删除数据,但是禁止删除其他用户的数据;
id命令可以查看实际的和有效的用户与组ID;
判断文件的类型常使用以下宏函数;
S_ISDIR(m) //判断是否是目录文件;
S_ISREG(m) //判断是否是普通文件;
S_ISLNK(m) //是否软链接;
S_ISBLK(m) //是否块设备;
S_ISCHR(m) //是否字符设备;
S_ISSOCK(m) //是否Unix域套接字;
S_ISFIFO(m) //是否有名管道;
其中m表示获取的st_mode;
文件系统的基本概念
1.在Unix操作系统中,每个文件以一个或多个数据块离散的存放在磁盘的块表区中(柱面表区);
2.一个文件的每个数据块的存储位置保存在I节点(i-node)中;
3.对应系统中多个文件的I节点保存在磁盘的I节点表中,每个I节点通过一个下标唯一地标识,这个下标称为I节点号;
4.文件的名称和其所对应的I节点号保存在目录文件中;
5.访问文件的过程:根据文件名到目录文件中找到相应的I节点号,再根据I节点号到I节点表中找到对应的I节点,根据I节点找到每个数据块在磁盘上的存储位置,进而访问其中的数据;
6.根目录/的I节点信息保存在一个特殊的位置即超级块super block中,每次挂载(mount)文件系统时首先找到根目录的位置;
7.以上所述文件数据的存储结构和访问方式,被称为文件系统fs;
格式化硬盘的过程实际就是重新建立文件系统的过程;
一切皆文件
fscanf(stdin, ...);
fprintf(stdout, ...);
Linux环境下文件具有特殊的意义,它为系统中的服务和设备提供了一种抽象接口;
程序完全可以向访问普通文件一样,访问串口/网络/鼠标/键盘/打印机等各种设备;
大多数情况下只需要使用五个最基本的系统调用
open()/close()/read()/write()/ioctl();
广义文件
1-> 目录文件
2-> 设备文件
控制台 /dev/console
声卡 /dev/audio
标准输入输出 /dev/tty
空设备 /dev/null
文件相关的系统调用
open() //用来打开/创建文件,类似于fopen();
close() //用来关闭文件,类似于fclose();
read() //用来读取文件,类似于fread();
write() //用来写入文件,类似于fwrite();
lseek() //设置读写位置,类似于fseek()/ftell();
creat() //创建文件;
fcntl() //修改文件属性;
unlink() //删除硬链接文件;
rmdir() //删除空目录
remove() //删除硬链接(unlink)/删除空目录(rmdir);
注意
1.如果被unlink/remove删除的是文件的最后一个硬链接,并且没有进程正在打开该文件,那么该文件在磁盘上的存储区域将被立即标记为free;
反之,如果有进程正在打开该文件,那么该文件在目录中条目虽然被立即删除,但它在磁盘上的存储区域将在所有打开它的进程关闭该文件以后才会被标记为free;
2.如果被unlink/remove删除的是一个软链接文件,那么仅软链接本身被删除,源文件不受影响;
创建硬链接相当于创建了一个别名,还是统一块数据区域,可以用ls -i命令查看,inode节点号是一样的;
文件描述符
int fd = open(...);
1.文件描述符的本质就是内核为每个进程维护的文件描述符表中存储的文件表指针的下标;//深刻理解
文件描述符表(在内核层)中存储的文件表指针指向"文件表";
文件表中存储着文件的状态和位置信息以及v-node指针;
v-node指针指向v-node表(内核层);
v-node表中存放着磁盘中i-node表的拷贝;
访问文件时会根据I节点号到I节点表中找到对应的I节点,根据I节点找到每个数据块在磁盘上的存储位置,进而访问其中的数据;
+----+ +------------+ +------+ +--------+ +------------+
| fd |->|文件描述符表| |文件表| |v-node表| |磁盘i-node表|
| | |以文件描述符| | 状态 | | | | |
| | |为下标的文件| | 位置 | | inode | | |
| | |表指针 |->|v-node| | 的拷贝 |->| |
| | | | |指针 |->| | | |
+----+ +------------+ +------+ +--------+ +------------+
grep __IO_FILE /usr/include/*.h
/usr/include/stdio.h:typeded struct __IO_FILE FILE;
grep _fileno /usr/include/*.h -n
/usr/include/libio.h:294: int _fileno;
vi /usr/include/libio.h +294
在标准C语言中FILE *fp = fopen();
_fileno是FILE的其中一个成员,而_fileno就是文件描述符;
2.文件描述符是一个非负的整数;
3.每个文件描述符对应一个打开的文件;
4.文件描述符从open函数返回,同时作为参数传递给后续的文件函数(read/write/close...)使用;
5.内核缺省为每个进程打开三个文件表述符:
标准输入0 STDIN_FILENO
标准输出1 STDOUT_FILENO
标准错误输出2 STDERR_FILENO
#include <unistd.h>
/* * 文件描述符演示 */ #include <stdio.h> #include <unistd.h> int main() { printf("stdin->_fileno: %d ", stdin->_fileno); //0 printf("stdout->_fileno: %d ", stdout->_fileno); //1 printf("stderr->_fileno: %d ", stderr->_fileno); //2 FILE *fp = fopen("a.out", "r"); printf("fp->_fileno: %d ", fp->_fileno); //3 fclose(fp); FILE *fp1 = fopen("a.out", "r"); printf("fp1->_fileno: %d ", fp1->_fileno); //3 fclose(fp1); printf("一个进程同时最多打开的文件描述符个数是%ld ", sysconf(_SC_OPEN_MAX)); printf("fclose(stdout); "); fclose(stdout); printf("hello world !"); //等价与fprintf(stdout, "hello world ! "); return 0; }
6.可以在输入输出重定向时指定定向文件描述符;
/* * 重定向练习 * ./a.out 0<i.txt 1>o.txt 2>e.txt */ #include <stdio.h> int main() { int i; printf("please input a number: "); fscanf(stdin, "%d", &i); fprintf(stdout, "标准输出: %d ", i); fprintf(stderr, "标准出错: %d ", i); return 0; }
7.一个进程可以使用的文件描述符的范围是0到OPEN_MAX;OPEN_MAX宏在早期的Unix/Linux中被定义为63,所以一个进程同时最多只能打开64个文件;现在的Unix/Linux系统可以同时打开更多文件;
通过sysconf(_SC_OPEN_MAX)调用可以获取当前系统所允许的进程最多同时打开的文件描述符个数;
Unix系统文件相关的系统调用
open()/creat()/close()
对应的头文件是#include <fcntl.h>
int open(
const char *pathname, //文件路径;
int flags, //状态标志;
mode_t mode //权限模式,只在创建时有效;
); //打开/创建文件都可以使用此函数;
flags为以下值的位或
O_RDONLY //只读|
O_WRONLY //只写|->三个只能选一个;
O_RDWR //读写|
O_APPEND //追加,注意追加不可以和只读放在一起,否则冲突;
O_CREAT //创建,不存在即创建;已存在,直接打开,保留原有内容;
//除非位或了下面的标志...;
O_EXCL //排斥exclude,已存在则失败,防止意外覆盖原有文件;
//可以用errno来判断失败的原因;
O_TRUNC //清空,已存在则清空;
//O_EXCL/O_TRUNC只能选一个与0_CREAT位或;
mode的取值与chmod命令的权限参数一致,均为八进制;
open()成功则返回文件描述符,失败则返回-1;
int creat(
const char *pathname, //文件路径;
mode_t mode //权限模式
//仅创建有效
); //常用于创建文件
成功返回文件描述符,失败返回-1;相当于这样调用open函数:
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
关闭文件的实质是释放内核中的文件描述符表;
int close(int fd); //成功返回0,失败返回-1;
实际创建文件的权限 = mode & ~umask;
因此所有umask(权限掩码)中包含的权限,在创建的文件中都不会存在;
如:0666 & ~0002 == 0664;
/* * open()函数练习 */ #include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { /* 创建文件,读写权限,如果不存在就创建,已存在就清空 */ int fd1 = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd1 == -1) { perror("open"); return -1; } printf("文件描述符fd1 = %d ", fd1); //3 /* 以只读模式再次打开文件 */ int fd2 = open("test.txt", O_RDONLY); //open的参数表是可变的; if (fd2 == -1) { perror("open"); return -1; } printf("文件描述符fd2 = %d ", fd2); //4 close(fd2); close(fd1); return 0; }
write()
#include <unistd.h>
ssize_t write(
int fd, //文件描述符;
const void *buf, //操作地址;从buf写入到fd对应文件;
size_t count //期望写入字节数;
); //成功时返回实际写入字节数,失败则返回-1;
ssize_t //int
size_t //尽量使用这个类型,会随系统而变;
32位中为 unsigned int
64位中为 unsigned long int
/* * write()函数练习 */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main() { //创建文件 int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd == -1) { perror("open"); return -1; } const char *text = "Hello World"; //指定缓冲区 printf("写入内容: %s ", text); /* 定义期望写入字节数 */ size_t towrite_num = strlen(text) * sizeof(text[0]); ssize_t written_num = write(fd, text, towrite_num); //write if (written_num == -1) { perror("write"); return -1; } printf("期望写入%d字节,实际写入%d字节. ", towrite_num, written_num); close(fd); //关闭文件 return 0; }
read()
#include <unistd.h>
ssize_t read(
int fd, //文件描述符
void *buf, //操作缓冲区;//从fd对应文件读取到buf中;
size_t count //期望读取的字节数;
); //成功时返回实际读取字节数;
读到文件尾返回0,如果读取失败返回-1;
标C中有一个函数feof();可以判断是否读到文件尾,如果是则返回1;
/* * read()函数练习 */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("test.txt", O_RDONLY); //以只读方式打开; if (fd == -1) { perror("open"); return -1; } char text[256]; //缓冲区; size_t toread = sizeof(text); //期望读取字节数; ssize_t readed = read(fd, text, toread); //read if (readed == -1) { perror("read"); return -1; } /* 设置结尾,防止后面垃圾信息干扰 */ text[readed / sizeof(text[0])] = " "; printf("期望读取%d字节,实际读取%d字节 ", toread, readed); printf("读取的内容是: %s ", text); close(fd); //关闭文件 return 0; }
练习
写一个文件拷贝程序,带有覆盖检查功能;
提示
在一个循环中,读一块,写一块,读完结束;
调用open函数打开目标文件时,给O_WRONLY|O_CREAT|O_EXCL,若目标文件存在,则open失败,判断errno == EEXIST,说明目标已经存在;
/* * 写一个文件拷贝程序,带有覆盖检查功能 */ #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <sys/stat.h> int main(int argc, char *argv[]) { if (argc < 3) { printf("用法: %s <源文件> <目标文件> ", argv[0]); return -1; } /* 以只读方式打开源文件 */ int src = open(argv[1], O_RDONLY); if (src == -1) { perror("open"); return -1; } struct stat srcfile_st; /* 取源文件的权限 */ if (fstat(src, &srcfile_st) == -1) { perror("fstat"); return -1; } /* 以源文件的权限,读写方式创建目标文件 */ int dst = open(argv[2], O_WRONLY | O_CREAT | O_EXCL, srcfile_st.st_mode); if (dst == -1) { /* 如果返回失败,判断错误类型 */ if (errno != EEXIST) { perror("open"); return -1; } //目标已经存在 printf("文件%s已经存在,是否覆盖?(Y/N)", argv[2]); int ch = getchar(); //只取一个字符 if (ch != "y" && ch != "Y") { return 0; } /* 重新创建文件,如果存在则清空 */ if ((dst = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, srcfile_st.st_mode)) == -1) { perror("open"); return -1; } } char buf[1024] = {}; ssize_t bytes; /* 循环读写,注意运算符优先级,最好加小括号标识 */ /* while (bytes = read(src, buf, sizeof(buf)) > 0) { //错误 */ while ((bytes = read(src, buf, sizeof(buf))) > 0) { printf("读取内容到地址%p ", buf); printf("读取字节%d ", bytes); if (write(dst, buf, bytes) == -1) { perror("write"); return -1; } } if (bytes == -1) { perror("read"); return -1; } /* 关闭文件 */ close(dst); close(src); return 0; }
练习
分别以二进制和纯文本方式读写文件;
二进制文件与纯文本文件取决于文件的存储方式,一个是存储数据本身,一个是存储数据格式化成文本之后的数据;
hexdump -C txt.txt 命令可以以16进制查看文件
/* * UNIX C * uc_bintxt.c */ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> typedef struct Employee { char name[256]; int age; double salary; } EMP; /* 以二进制的方式写test.bin */ void bin_write(void) { char name[256] = "张飞"; int age = 30; double salary = 20000.5; EMP emp = { "赵云", 20, 10000.5 }; int fd = open("test.bin", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd == -1) { perror("open"); exit(-1); } if (write(fd, name, sizeof(name)) == -1 || write(fd, &age, sizeof(age)) == -1 || write(fd, &salary, sizeof(salary)) == -1 || write(fd, &emp, sizeof(emp)) == -1) { perror("write"); close(fd); exit(-1); //exit()是有尊严的自杀,kill()是杀别人; } close(fd); } /* 以二进制的方式读test.bin */ void bin_read(void) { char name[256]; int age; double salary; EMP emp; int fd = open("test.bin", O_RDONLY); if (fd == -1) { perror("open"); exit(-1); } if (read(fd, name, sizeof(name)) == -1 || read(fd, &age, sizeof(age)) == -1 || read(fd, &salary, sizeof(salary)) == -1 || read(fd, &emp, sizeof(emp)) == -1) { perror("read"); close(fd); exit(-1); } close(fd); printf("%s %d %lg ", name, age, salary); printf("%s %d %lg ", emp.name, emp.age, emp.salary); } /* 在标准C语言中可以直接使用fprintf()实现以文本方式写文件; */ /* 在UC中需要使用函数进行转换; */ /* 以纯文本方式写test.txt */ void txt_write(void) { char name[256] = "张飞"; int age = 30; double salary = 20000.5; EMP emp = { "赵云", 20, 10000.5 }; char text[1024]; /* 以二进制形式写文本,就会显示出文本 */ /* 实际会写入文本的ASCII码 */ snprintf(text, sizeof(text), "%s %d %lg %s %d %lg", name, age, salary, emp.name, emp.age, emp.salary); //防止数据溢出 /* 在内存中格式化字符串 */ /* sprintf(text, "%s %d %lg %s %d %lg", * name, age, salary, * emp.name, emp.age, emp.salary); */ int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd == -1) { perror("open"); exit(-1); } if (write(fd, text, strlen(text) * sizeof(text[0])) == -1) { perror("write"); close(fd); exit(-1); } close(fd); } /* 以纯文本方式读test.txt */ void txt_read(void) { char name[256]; int age; double salary; EMP emp; int fd = open("test.txt", O_RDONLY); if (fd == -1) { perror("open"); exit(-1); } char text[1024] = { }; if (read(fd, text, sizeof(text)) == -1) { perror("read"); exit(-1); } close(fd); /* printf("%s ", text); */ /* text就是读取到的文本 */ /* 用sscanf()提取字符串 */ sscanf(text, "%s%d%lf%s%d%lf", name, &age, &salary, emp.name, &emp.age, &emp.salary); printf("%s %d %lg %s %d %lg ", name, age, salary, emp.name, emp.age, emp.salary); } int main(void) { bin_write(); bin_read(); txt_write(); txt_read(); return 0; }
在标准C语言中可以直接调用fprintf()函数以文本方式写入文件;
使用fgets()一次读取文件的一行;
使用feof()函数判断是否已经读到文件末尾;
/* * standard C * std_bintxt.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct Employee { char name[256]; int age; double salary; } EMP; /* 以二进制的方式写test.bin */ void bin_write(void) { char name[256] = "张飞"; int age = 30; double salary = 20000.5; EMP emp = { "赵云", 20, 10000.5 }; FILE *fp = fopen("test.bin", "wb"); if (fp == NULL) { exit(-1); } fwrite(name, sizeof(name), 1, fp); fwrite(&age, sizeof(age), 1, fp); fwrite(&salary, sizeof(salary), 1, fp); fwrite(&emp, sizeof(emp), 1, fp); fclose(fp); fp = NULL; } /* 以二进制的方式读test.bin */ void bin_read(void) { char name[256]; int age; double salary; EMP emp; FILE *fp = fopen("test.bin", "rb"); if (fp == NULL) { exit(-1); } fread(name, sizeof(name), 1, fp); fread(&age, sizeof(age), 1, fp); fread(&salary, sizeof(salary), 1, fp); fread(&emp, sizeof(emp), 1, fp); fclose(fp); fp = NULL; printf("%s %d %lg ", name, age, salary); printf("%s %d %lg ", emp.name, emp.age, emp.salary); } /* 在标准C语言中可以直接使用fprintf()实现以文本方式写文件; */ /* 在UC中需要使用函数进行转换; */ /* 以纯文本方式写test.txt */ /* fprintf() */ void txt_write(void) { char name[256] = "张飞"; int age = 30; double salary = 20000.5; EMP emp = { "赵云", 20, 10000.5 }; char text[1024]; FILE *fp = fopen("test.txt", "wb"); if (fp == NULL) { exit(-1); } /* 以二进制形式写文本,就会显示出文本 */ /* 实际会写入文本ASCII码的二进制数据 */ fprintf(fp, "%s %d %lg %s %d %lg", name, age, salary, emp.name, emp.age, emp.salary); fclose(fp); fp = NULL; } /* 以纯文本方式读test.txt */ /* fgets() + feof() */ void txt_read(void) { char name[256]; int age; double salary; EMP emp; FILE *fp = fopen("test.txt", "rb"); if (fp == NULL) { exit(-1); } char text[1024] = { }; char *tmp = text; while (!feof(fp)) { /* fgets一次读取一行 */ fgets(tmp, sizeof(text), fp); /* text就是读取到的文本 */ if (strstr(text, "赵云")) { /* 在text字符串中找到子字符串"赵云" */ printf("%s", text); if (!strstr(text, " ")) { /* 如果结果中没有换行符,打印换行 */ printf(" "); } } } fclose(fp); fp = NULL; } int main(void) { bin_write(); bin_read(); txt_write(); txt_read(); return 0; }
系统IO和标准IO
当应用程序调用系统函数时,需要切换用户状态和内核态,因此频繁调用会导致性能的损失;
标准库做了必要的优化,内部会维护一个缓冲区,只有满足特定条件时,才将缓冲与系统内核同步;借此可以降低系统调用的频率,减少进程在用户态和内核态之间切换的次数,提高运行效率;
/* * sys/std io test * sysio.c */ #include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("sysio.dat", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd == -1) { perror("open"); return -1; } unsigned int i; for (i = 0; i < 100000; ++i) { write(fd, &i, sizeof(i)); } close(fd); return 0; } /* * sys/std io test * stdio.c */ #include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { FILE *fp = fopen("sysio.dat", "wb"); if (!fp) { perror("fopen"); return -1; } unsigned int i; for (i = 0; i < 100000; ++i) { fwrite(&i, sizeof(i), 1, fp); } fclose(fp); return 0; }
使用以下命令编译和测试
gcc stdio.c -o stdio
gcc sysio.c -o sysio
time sysio
time stdio
发现stdio明显的比较快;
lseek()
每个打开的文件都有一个与之相关的"文件位置";
文件位置通常是一个非负的整数,用于度量从文件头开始计算的字节数;
所有的读写操作都从当前文件位置开始,并根据所读写的字节数增加文件位置;
打开一个文件时,除非指定了O_APPEND,否则文件位置一律被设为0;
lseek()函数仅修改文件表中记录的文件位置,并不引发任何IO动作;
lseek()函数可以将文件位置设置到文件尾之后,这样在后续写操作以后会在文件中形成空洞;文件空洞不占用磁盘空间,但会被计算在文件大小内;//下载软件在下载文件时候会首先建立相应大小的空文件;
#include <sys/types.h>
#include <unistd.h>
off_t lseek(
int fd, //文件描述符;
off_t offset; //偏移量;
int whence //起始位置;
); //成功返回调整以后的文件位置,失败返回-1;
whence取值
SEEK_SET //文件头(文件的第一个位置);
SEEK_CUR //从当前位置(下一次读写操作的位置);
SEEK_END //从文件尾(文件最后一个字节的下一个位置);
/* * lseek()函数练习 */ #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> int main() { int fd = open("seek.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd == -1) { perror("open"); return -1; } const char *text = "hello world !"; if (write(fd, text, strlen(text) * sizeof(text[0])) == -1) { perror("write"); return -1; } /* 读写都会改变文件当前位置 */ off_t pos = lseek(fd, 0, SEEK_CUR); if (pos == -1) { perror("lseek"); return -1; } printf("当前文件位置: %ld ", pos); //13 if (lseek(fd, -7, SEEK_CUR) == -1) { perror("lseek"); return -1; } pos = lseek(fd, 0, SEEK_CUR); printf("当前文件位置: %ld ", pos); //6 text = "Linux"; if (write(fd, text, strlen(text) * sizeof(text[0])) == -1) { perror("write"); return -1; } /* 从文件尾向后偏移,再写入将产生文件空洞 */ if (lseek(fd, 8, SEEK_END) == -1) { perror("lseek"); return -1; } text = " <-文件这里有空洞!"; if (write(fd, text, strlen(text)) == -1) { perror("write"); return -1; } /* 文件尾的位置也代表文件的大小 */ off_t size = lseek(fd, 0, SEEK_END); if (size == -1) { perror("lseek"); return -1; } printf("文件的大小是: %d字节 ", size); close(fd); return 0; }
dup()/dup2()函数
duplicate a file descripter;获得文件描述符的副本;
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
成功时返回文件描述符oldfd的副本(新的描述符),失败返回-1;
复制一个已打开文件的描述符;
dup()函数返回的一定是当前未使用的最小的文件描述符;
dup2()函数可以由第二个参数newfd指定期望复制到的文件描述符,若所指定的文件描述符已打开,则先关闭之,再复制;
所返回的文件描述符副本与源文件描述符oldfd对应同一个文件表;
文件描述符与文件并不是一一对应;
注意区分通过dup()/dup2()获得文件描述符副本,和两次open()同一个文件的区别
dup()/dup2()只复制文件描述符表中的文件表指针,不会重建新的文件表;
fd1 |->文件表->V节点(I节点信息表);
fd2 |
没有创建新的文件表;操作fd1和fd2会同步操作同一个文件;
open()会创建一个新的文件表,并为其分配新的文件描述符:
fd2->文件表 |->v节点(I节点信息表);
fd1->文件表 |
多次open()同一文件,同时操作fd1和fd2会相互覆盖文件内容
/* * dup()/dup2()函数演示 */ #include <stdio.h> #include <unistd.h> #include <string.h> #include <fcntl.h> int main() { int fd1 = open("dup.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd1 == -1) { perror("open"); return -1; } printf("fd1 = %d ", fd1); //3 int fd2 = dup(fd1); if (fd2 == -1) { perror("dup"); return -1; } printf("fd2 = %d ", fd2); //4 int fd3 = dup2(fd2, 100); if (fd3 == -1) { perror("dup2"); return -1; } printf("fd3 = %d ", fd3); //100 char buf[64] = "Hello, World !"; /* 用fd1写文件 */ if (write(fd1, buf, strlen(buf) * sizeof(buf[0])) == -1) { perror("write"); return -1; } /* 用fd2移动文件当前位置 */ if (lseek(fd2, -7, SEEK_CUR) == -1) { perror("lseek"); return -1; } const char *text = "Linux"; /* 用fd3再次写文件 */ if (write(fd3, text, strlen(text) * sizeof(text[0])) == -1) { perror("write"); return -1; } close(fd3); /* 关闭fd3后再用fd2移动位置 */ if (lseek(fd2, 0, SEEK_SET) == -1) { perror("lseek"); return -1; } /* 通过fd1读取文件内容 */ if (read(fd1, buf, 64)) { printf("%s "); } close(fd2); close(fd1); return 0; }
/* * open()与dup()/dup2()的方式不同 */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> int main() { int fd1 = open("same.txt", O_RDWR | O_CREAT | O_TRUNC, 0666); if (fd1 == -1) { perror("open"); return -1; } printf("fd1 = %d ", fd1); //3 int fd2 = open("same.txt", O_RDWR); //open会创建新的文件表; if (fd2 == -1) { perror("open"); return -1; } printf("fd2 = %d ", fd2); //4 const char *text = "Hello, world !"; if (write(fd1, text, strlen(text) * sizeof(text[0])) == -1) { perror("write"); return -1; } text = "Linux"; /* fd2与fd1对应同一个文件,但文件表不同 */ /* 后写入的会覆盖先写入的内容 */ if (write(fd2, text, strlen(text) * sizeof(text[0])) == -1) { perror("write"); return -1; } return 0; }
/* * dup()的使用示例 */ #include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("use_dup.log", O_WRONLY | O_CREAT | O_TRUNC, 0666); printf("关闭标准输出,重定向到文件 "); /* * close(STDOUT_FILENO); * //把标准输出的文件描述符释放,以便让dup占用 * dup(fd); //dup()占用了标准输出的文件描述符 */ dup2(fd, STDOUT_FILENO); //相当于以上两句的效果 /* 会把标准输出重定向到日志文件 */ printf("内存分配失败 "); printf("打开文件失败 "); /* 如果不用dup()/dup2(),可能需要修改所有的 */ /* printf()为fprintf();工作量很大,易出错; */ return 0; }
其实shell中的输出重定向操作就是通过这种方式来操作的;
sync()/fsync()/fdatasync()
fflush()
大多数的磁盘IO都要通过缓冲进行,写入文件其实只是写入缓冲区,直到缓冲区满,才将其排入队列;
延迟写降低了写操作的次数;提高写操作的效率,但是会导致磁盘文件与缓冲区数据不同步;
sync()/fsync()/fdatasync()用于强制磁盘文件与缓冲区同步;
sync()将把进程中所有被修改过的缓冲区排入写队列,立即返回,并不等待写磁盘操作完成; //不带参数
fsync()只针对一个文件,且一直等到写磁盘操作完成才返回;
fdatasync()只同步文件数据,不同步文件属性;
#include <unistd.h>
void sync(void);
int fsync(int fd);
int fdatasync(int fd);/成功返回0,失败返回-1;
应用程序内存
| |
fwrite() |
| |
V |
标准库缓冲 write()
| |
fflush() |
V V
内 核 缓 冲 区
|
sync()/fsync()/fdatasync()
|
V
写队列/磁盘设备
fcntl()
操作文件描述符
#include <unistd.h>
#include <fcntl.h>
int fcntl(
int fd, //文件描述符
int cmd, //控制指令
... //可变参数,因控制指令而异
); //成功的返回值因cmd而异,失败返回-1;
对fd所表示的文件执行cmd所表示的控制,某些控制需要提供参数,某些控制会返回特定的值;
1.复制文件描述符
int fd = fcntl(oldfd, F_DUPDF); //返回新的文件描述符
int fd = fcntl(oldfd, F_DUPFD, newfd); //返回新的文件描述符
复制oldfd为不小于newfd的文件描述符;
若newfd文件描述符已用,该函数就会选择比newfd大的最小未用值,而非dup2()函数那样关闭之;
/* * fcntl()的F_DUPFD命令 */ #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { int fd1 = open("test1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd1 == -1) { perror("open"); return -1; } printf("fd1 = %d ", fd1); int fd2 = open("test2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd2 == -1) { perror("open"); return -1; } printf("fd2 = %d ", fd2); /* * int fd3 = dup2(fd1, fd2); * //先关闭fd2,然后将fd1复制到fd2; * //以后fd2和fd2都代表fd1所代表的文件; * if (fd3 == -1) { * perror("dup2"); * return -1; * } */ /* int fd3 = fcntl(fd1, F_DUPFD, fd2); */ //不关闭fd2,而是将fd1复制到fd2的下一个空闲位置; if (fd3 == -1) { perror("fcntl"); return -1; } printf("fd3 = %d ", fd3); const char *text = "123"; if (write(fd1, text, strlen(text)*sizeof(text[0])) == -1) { perror("write"); return -1; } text = "456"; if (write(fd2, text, strlen(text)*sizeof(text[0])) == -1) { perror("write"); return -1; } text = "789"; if (write(fd3, text, strlen(text)*sizeof(text[0])) == -1) { perror("write"); return -1; } close(fd3); close(fd2); close(fd1); return 0; }
2.获取/设置文件状态标志
一个状态标志占一个位;
int flags = fcntl(fd, F_GETFL); //get flag
if (flags & O_RDONLY) {
/* 位操作,flags中O_RDONLY位为1 */
} else {
/* flags中O_RDONLY位为0 */
}
不能获取O_CREAT|O_EXCL|O_TRUNC与创建有关的属性;
能访问就说明文件已经存在了,跟创建相关的已经没有必要获取;
int res = fcntl(fd, F_SETFL, flags); //追加flags标志;
在现有标志的基础上追加flags标志;
但只能追加O_APPEND|O_NONBLOCK属性;
/* * fcntl()文件属性操作 */ #include <stdio.h> #include <unistd.h> #include <fcntl.h> struct { int flag; const char *desc; } flist[] = { {O_RDONLY, "O_RDONLY"}, //只读 {O_WRONLY, "O_WRONLY"}, //只写 {O_RDWR, "O_RDWR"}, //读写模式 {O_CREAT, "O_CREAT"}, //创建 {O_EXCL, "O_EXCL"}, //排他模式 {O_NOCTTY, "O_NOCTTY"}, //not control tty {O_TRUNC, "O_TRUNC"}, //覆盖模式 {O_APPEND, "O_APPEND"}, //追加模式 {O_NONBLOCK, "O_NONBLOCK"}, //阻塞方式 {O_DSYNC, "O_DSYNC"}, //数据同步 {O_ASYNC, "O_ASYNC"}, //异步操作 {O_RSYNC, "O_RSYNC"}, //读同步 {O_SYNC, "O_SYNC"}, //同步 }; void print_all_flags() { int i = 0; for (i = 0; i < sizeof(flist) / sizeof(flist[0]); ++i) { printf("[0x%8x]%s ", flist[i].flag, flist[i].desc); } } void print_flags(int flags) { printf("文件状态标志(0X%08X):", flags); size_t i; for (i = 0; i < sizeof(flist) / sizeof(flist[0]); ++i) { if (flags & flist[i].flag) { printf("%s ", flist[i].desc); } } printf(" "); } int main() { print_all_flags(); int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC | O_ASYNC, 0666); /* O_ASYNC异步IO操作 */ if (fd == -1) { perror("open"); return -1; } int flags = fcntl(fd, F_GETFL); //获取文件flags; if (flags == -1) { perror("flags"); return -1; } print_flags(flags); //不能获取O_CREAT|O_EXCL|O_TRUNC if (fcntl(fd, F_SETFL, O_RDWR | O_APPEND | O_NONBLOCK) == -1) { //只能追加O_APPEND | O_NONBLOCK perror("fcntl"); return -1; } if ((flags = fcntl(fd, F_GETFL)) == -1) { perror("fcntl"); return -1; } print_flags(flags); //只能追加O_APPEND|O_NONBLOCK close(fd); return 0; }
3.给文件加锁
假设有一个文件同时被两个进程访问该文件的A区和B区,但是A区和B区有重叠;
第1种情况 在写锁区上加写锁;
-----------------------------------------------------------
进程1 进程2
打开文件,准备写A区; 打开文件,准备写B区;
调用fcntl给A区加写锁
fcntl返回成功,A区被
加上写锁;
调用write写A区; 调用fcntl给B区加写锁,fcntl阻塞
或返回失败;
调用fcntl,解锁A区; fcntl返回成功,B区被加上写锁;
调用write,写B区;
调用fcntl解锁B区;
关闭文件; 关闭文件;
===========================================================
第2种情况 在写锁区上加读锁
------------------------------------------------------------
进程1 进程2
打开文件,准备写A区 打开文件,准备读B区
调用fcntl给A区加写锁
fcntl返回成功,A区被
加上写锁;
调用write写A区, 调用fcntl给B区加读锁,fcntl阻塞
或返回失败;
调用fcntl,解锁A区; fcntl返回成功,B区被加上读锁;
调用read,读B区;
调用fcntl解锁B区;
关闭文件; 关闭文件;
===========================================================
第3种情况 在读锁区上加写锁
----------------------------------------------------------
进程1 进程2
打开文件,准备读A区 打开文件,准备写B区
调用fcntl给A区加读锁
fcntl返回成功,A区被
加上读锁;
调用read读A区; 调用fcntl给B区加读锁,fcntl阻塞
或返回失败;
调用fcntl,解锁A区; fcntl返回成功,B区被加上写锁;
调用write,写B区;
调用fcntl解锁B区;
关闭文件; 关闭文件;
===========================================================
第4种情况 在读锁区上加读锁
------------------------------------------------
进程1 进程2
打开文件,准备读A区; 打开文件,准备读B区;
调用fcntl给A区加读锁
fcntl返回成功,A区被
加上读锁;
调用read读A区; 调用fcntl给B区加读锁,
fcntl返回成功;
B区被加上读锁;
调用fcntl,解锁A区; 掉用read,读B区;
调用fcntl解锁B区;
关闭文件; 关闭文件;
===========================================================
试图在区域上加
读锁 写锁
区域上没有任何锁 OK OK
区域上已经有读锁 OK NO
区域上已经有写锁 NO NO
读锁也称为共享锁,写锁也称为独占锁(或排他锁);
无论是读锁还是写锁都是协议锁(或君子锁,劝谏锁);
如果有人不检查直接就write()也是没用的;
int fcntl(int fd,
F_SETLKW/*会等待*//F_SETLK/*不等待*/,
struct flock *lock
);
struct flock{
short int l_type;//锁类型;
//F_RDLCK-读锁
//F_WRLCK-写锁
//F_UNLCK-解锁
short int l_whence;//锁区偏移起点;
//SEEK_SET-文件头
//SEEK_CUR-当前位
//SEEK_END-文件尾
off_t l_start;//锁区偏移;
//从l_whence开始计算;
off_t l_len;//锁区长度;
//0表示锁到文件尾;
pid_t l_pid;//进程加锁,若加锁给-1;
};
例如
1->给文件从距离文件头10个字节处锁到第40个字节处,加写锁;
struct flock lck;
lck.l_type = F_WRLCK; //F_UNLCK时是解锁;fcntl中不需要更改参数;
lck.l_whence = SEEK_SET;
lck.l_start = 10;
lck.l_len = 30;
lck.l_pid = -1;
fcntl(fd, F_SETLKW, &lck);
如果成功返回0,如果失败返回-1;
2->给文件解锁;
struct flock lck;
lck.l_type = F_UNLCK; //解锁
lck.l_whence = SEEK_SET;
lck.l_start = 10;
lck.l_len = 30;
lck.l_pid = -1;
fcntl(fd, F_SETLKW, &lck);
以F_SETLK方式加锁,fcntl不会阻塞;
如果加锁失败返回-1,此时的errno == EACCES/EAGAIN;
如果加了锁但是没解锁,如果进程结束了或文件描述符被关闭了,进程里边跟该文件相关的协议锁自然就消失了;
文件锁标志是保存在v-node中的,并不是保存在文件表中,因为不同的进程open()同一个文件的时候,操作的是不同的文件表,但却都对应一个v-node;当加锁或解锁的时候都会检查v-node,并在加锁时添加到某个链表里;如果进程结束,文件表也就消失了,内核也会删掉对应进程的文件锁;
在v-node中一个文件可能会被不同的进程打开并加锁,加锁将按进程信息保存在某个链表中,进程操作时就会去检查锁的标志;如果某个进程没有锁协定,就不受锁的限制;
/* * 文件写锁 */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> /* 加写锁的函数 */ int wlock(int fd, off_t start, off_t len, int wait) { /* wait传递要不要使用阻塞方式; */ struct flock lock; lock.l_type = F_WRLCK; //写锁 lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, wait ? F_SETLKW : F_SETLK, &lock); /* 只有当fcntl()返回,函数才会拿到返回值return; */ /* 如果fcntl()阻塞,函数也将阻塞在这里; */ } /* 解锁 */ int ulock(int fd, off_t start, off_t len) { struct flock lock; lock.l_type = F_UNLCK; //解锁 lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, F_SETLK, &lock); } int main(int argc, char *argv[]) { if (argc < 2) { printf("用法: %s <字符串> ", argv[0]); return -1; } int fd = open("test.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); /* 如果文件没有则创建,有的话就追加; */ if (fd == -1) { perror("open"); return -1; } /* 给整个文件加写锁 */ if (wlock(fd, 0, 0, 1) == -1) { perror("wlock"); return -1; } size_t i; /* 写操作 */ for (i = 0; argv[1][i]; ++i) { if (write(fd, &argv[1][i], sizeof(argv[1][i])) == -1) { perror("write"); return -1; } printf("%#x ", (unsigned char)argv[1][i]); sleep(1); } /* 解锁 */ if (ulock(fd, 0, 0) == -1) { perror("ulock"); return -1; } close(fd); return 0; }
文件写锁测试,编译后在不同的终端同时写,后一个进程将等待第一个进程解锁后才能继续写操作;
/* * 文件读锁 */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> /* 加读锁 */ int rlock(int fd, off_t start, off_t len, int wait) { struct flock lock; lock.l_type = F_RDLCK; //加读锁 lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, wait ? F_SETLKW : F_SETLK, &lock); } /* 解锁 */ int ulock(int fd, off_t start, off_t len) { struct flock lock; lock.l_type = F_UNLCK; //解锁 lock.l_whence = SEEK_SET; lock.l_start = start; lock.l_len = len; lock.l_pid = -1; return fcntl(fd, F_SETLK, &lock); } int main() { int fd = open("test.txt", O_RDONLY); if (fd == -1) { perror("open"); return -1; } /* * if (rlock(fd, 0, 0, 1) == -1) { * //阻塞方式等待加读锁成功 * //傻傻的等待 * perror("rlock"); * return -1; * } */ /* 进程阻塞的时候将被排到进程调度队列之外, * 处于等待唤醒状态,不占用处理器资源; */ /* 如果文件已经被加写锁,再次加读锁时将失败 */ while (rlock(fd, 0, 0, 0) == -1) { /* 采用非阻塞方式,出错时返回-1 */ if (errno != EACCES && errno != EAGAIN) { //其他未知错误 printf("rlock"); return -1; } //加读锁失败 printf("该文件正被锁定,空闲处理... "); sleep(1); } char buf[1024]; ssize_t readed; while ((readed = read(fd, buf, sizeof(buf))) > 0) { write(STDOUT_FILENO, buf, readed); } printf(" "); if (readed == -1) { perror("read"); return -1; } /* 解锁,文件已经读取结束锁不再使用了 */ if (ulock(fd, 0, 0) == -1) { perror("ulock"); return -1; } return 0; }
文件读锁测试,编译后在不同的终端一个写,一个读,读操作将等待写操作解锁后才能继续读操作;
stat()/fstat()/lstat()
获取文件属性(元数据);
#include <sys/stat.h>
int stat(
const char *path; //文件路径;
struct stat *buf; //文件属性;
);
int fstat(
int fd, //文件描述符;
struct stat *buf; //文件属性;
);
int lstat(
const char *path; //文件路径;
struct stat *buf; //文件属性;
); //不跟踪软链接
第一个参数叫输入参数,第二个参数较输出参数;
返回值0表示成功,-1表示失败;执行结果由输出参数获取;
fstat()函数需要首先打开文件,传入文件描述符;其他两个函数不需要打开;
当用stat()操作一个软链接文件,获取的是软链接目标文件的属性;
而用lstat()操作一个软链接文件,获取的是软链接本身的属性;
struct stat {
dev_t st_dev; //设备ID;
ino_t st_ino; //inode;
mode_t st_mode; //权限和类型;
nlink st_nlink; //硬链接数;
uid_t st_uid; //属主ID;
gid_t st_gid; //属组ID;
dev_t st_rdev; //特殊设备ID;
off_t st_size; //总字节数;
blksize_t st_blksize; //I/O块字节数;
blkcnt_t st_blocks; //占用块(512B)数;windows叫簇,linux叫块
time_t st_atime; //最后访问时间;
time_t st_mtime; //最后修改时间;
time_t st_ctime; //最后状态改变时间;
};
其中st_mode(0TTSUGO) //第一个为数字零表示八进制;后一个是大写字母o;
TT //文件的类型; 6bit
S_IFDIR //目录类型(d); directory
S_IFREG //普通文件类型(-); regular
S_IFLNK //软链接(l); link
S_IFBLK //块设备(b); block
S_IFCHR //字符设备(c); character
S_IFSOCK //Unix域套接字(s); socket
S_IFIFO //有名管道(p); first in fist out
S //附加模式(可执行位); 3bit
S_ISUID //设置用户ID位(属主可执行位x -> s/S);
S_ISGID //设置组ID位(属组可执行位x -> s/S);
S_ISVTX //粘滞位(其他用户可执行位x -> t/T);
//-rwsr-sr-x
//drwxrwxrwt (如/tmp)
U //属主的权限; 3bit
S_IRUSR //属主可读
S_IWUSR //属主可写
S_IXUSR //属主可执行
G //属组的权限; 3bit
S_IRGRP //属组可读
S_IWGRP //属组可写
S_IXGRP //属组可执行
O //其他用户的权限; 3bit
S_IROTH //其他用户可读
S_IWOTH //其他用户可写
S_TXOTH //其他用户可执行
1.任何用户登录系统,都会启动一个shell进程,该进程的属主和属组取决于登录用户的UID和GID;
2.由登录shell启动的任何进程,其属主和属组都继承自该shell进程,此UID和GID被称为实际UID和实际GID;
3.决定一个进程对系统的访问能力的是有效UID和有效GID,一般情况下进程的有效UID和有效GID与其实际UID和实际GID一样;
4.如果一个可执行程序带有设置用户ID或者设置组ID位,那么运行该程序的进程的有效UID和有效GID就取自"该程序的属主和属组";
ls -l /usr/bin/passwd
-rwsr-xr-x. 1 root root 25980 2月 22 2012 /usr/bin/passwd
该文件只有root才可以修改,但是有s属性,普通用户也可以运行该命令修改自己的密码;
5.所谓粘滞位,就是具有该位的目录下面的文件只能被该文件的属主或者是root删除或更名;
粘滞位只能指针目录设置,对于文件无效;设置了粘滞位权限的目录,使用ls命令查看其属性时,其他用户权限处的x将变成t;使用chmod命令设置目录权限时,o+t,o-t权限模式可分别用于添加/移除粘滞位权限;
当目录被设置了粘滞位权限以后,即便用户对该目录有写权限,也不能删除该目录下的其他用户的文件数据,而只能有该文件的所有者和root用户才能有权删除;
对目录设置粘滞位以后,允许各用户在目录中任意写入删除数据,但是禁止删除其他用户的数据;
id命令可以查看实际的和有效的用户与组ID;
判断文件的类型常使用以下宏函数;
S_ISDIR(m) //判断是否是目录文件;
S_ISREG(m) //判断是否是普通文件;
S_ISLNK(m) //是否软链接;
S_ISBLK(m) //是否块设备;
S_ISCHR(m) //是否字符设备;
S_ISSOCK(m) //是否Unix域套接字;
S_ISFIFO(m) //是否有名管道;
其中m表示获取的st_mode;
声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。
- 上一篇: 对C语言的写文件操作fwrite的一个初学者常见误解
- 下一篇: 文件操作中的lseek函数详解