文件和目录
前言
本章讨论文件属性和文件系统内容。除了上一章讨论的普通文件,Linux的文件概念还包括:目录、设备等。在Linux系统中,文件的种类包括:普通文件、目录、符号链接、块设备、字符设备、管道、套接字。
本章讨论的主要内容为普通文件、目录和符号链接。它们的公共特点是,真实的保存在了硬盘中,而其它类型的文件是内核产生的文件,在硬盘中并不存在。
文件属性
通过stat函数或者stat命令可以获得文件属性。
文件属性 | 解释 |
---|
dev_t st_dev | 设备号 |
ino_t st_ino | inode编号 |
mode_t st_mode | 访问权限相关 |
nlink_t st_nlink | 硬链接数量 |
uid_t st_uid | 拥有该文件的用户 |
gid_t st_gid | 拥有该文件的组 |
dev_t st_rdev | 设备号 |
off_t st_size | 文件尺寸 |
blksize_t st_blksize | 文件系统的IO尺寸 |
blkcnt_t st_blocks | 占用的block数量,一个block为512字节 |
time_t st_atime | 最后访问时间 |
time_t st_mtime | 最后修改时间 |
time_t st_ctime | 最后文件状态修改时间 |
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
struct stat buf;
int ret = stat(".", &buf);
if(ret < 0)
{
perror("stat");
return 0;
}
if(S_ISREG(buf.st_mode))
{
printf("this is regular file\n");
}
else if(S_ISDIR(buf.st_mode))
{
printf("this is dir\n");
}
// 测试拥有该文件的账户是否有读权限
if(S_IRUSR & buf.st_mode)
{
printf("user can read\n");
}
open("a.txt", O_CREAT|O_RDWR, 0777);
// access("./a.out", R_OK|W_OK|X_OK);
printf("file size is %d\n", (int)buf.st_size);
getchar();
return 0;
}
3.3 文件类型
在前言中,提到文件类型包括七种,在stat结构题中,保存了文件的文件类型属性,它的文件类型属性保存在st_mode中。但是七种类型,只需要3位即可,而st_mode是一个整数,因此它还保存其它内容,如果需要判断一个文件属于何种类型,需要一些宏的帮助。

int main()
{
struct stat buf;
stat("a.txt", &stat);
if(S_ISREG(buf.st_mode))
{
printf("%s\n", "这是普通文件");
}
}
文件类型属性是只读的属性,无法修改。
3.4 用户和组
Linux是一个多用户操作系统,因此每个文件都有属性,记录着这个文件属于哪个用户/组。
用户/组信息可以被修改,可以通过chown来修改文件所属的用户和组信息。
修改文件所属用户和组,需要root权限。
新文件所属用户和组,是创建该文件的进程所属用户和组。
账户 | 解释 |
---|
实际账户 | 登陆系统时的账户 |
有效账户 | 决定进程的访问资源的账户 |
3.5 文件访问权限
文件使用了9个位来表示访问权限,和文件类型一起,保存在st_mode中。此9位分成3组,每组3个位,分别表示读/写/执行权限,而三个组分别表示拥有该文件的账户,拥有该文件的组,其它用户组的权限。如果对应位是1,表示有权限,如果是0表示没有权限。
文件访问权限经常用8进制来表示,比如上表的权限位可以表示为0755,意思是拥有它的账户对这个文件有读/写/执行权限,而拥有它的组有读/执行权限,其它账户对它有读/执行权限。
Linux提供一些宏,来测试文件的权限位

3.6 其它权限位
3.6.1 SUID
只能对文件设置,如果文件设置了该位,那么该文件被运行时,对应的进程的权限,不是运行该程序账户的权限,而是拥有该用户的权限。
在对文件
未设置SUID的情况下:
如果对文件
设置了SUID,那么:
可以通过chmod u+s
或者chmod u-s
来设置获取去除SUID。
设置SUID可以让一个普通账户拥有它不该有的权限,容易产生安全漏洞。
3.6.2 SGID
可以对文件和目录设置,如果对文件设置,那么它的作用类似SUID,不过影响的是组。
如果对目录设置,那么拷贝到该目录下的文件都会被置位,除非拷贝是带-p
参数。 在Ubuntu下测试并不如此。
在Ubuntu下设置了目录的SGID之后,在那个目录下创建的文件,拥有者是有效账户,而拥有组是该目录的拥有组。
命令:
chmod g+s
chmod g-s
3.6.3 StickyBit
可以对文件或者目录设置,如果对文件设置,那么当这个文件运行并退出后,系统依旧保留该文件对应的映象,这样这个程序再次运行时,就不需要加载再加载了。这个属性的作用并不大,因为它占用了内存。
如果对目录设置,那么表示在该目录下,账户只能删除和修改它自己创建的文件,对于其它账户创建的文件,它不能修改和删除。这个位作用比较大,在一些公共目录,往往有这个属性,比如/tmp
命令:
chmod o+t
chmod o-t
总结:
位 | 设置对象 | 设置方法 | 查看 | 效果 |
---|
SUID | 文件 | chmod u+s | 如果用户执行权限位为s或者S,则表示SUID有设置 | 当该文件被执行时,进程所拥有的权限是拥有该文件的账户权限 |
SUID | 目录 | 不可设置 | | |
SGID | 文件 | chmod g+s | 如果组执行权限位为s或者S,则表示GUID有设置 | 当执行该文件时,进程所属组是该拥有该文件的组 |
SGID | 目录 | chmod g+s | 同上 | 在该目录中创建文件时,该文件的所属组是目录的所属组 |
StickyBit | 文件 | chmod o+t | 如果其他执行权限位为t或者T,那么该文件有设置StickyBit | 执行该文件并退出后,系统保留该文件占用的一些内存,以便加快下一次的加载运行 |
StickyBit | 目录 | chmod o+t | 同上 | 账户只能修改和删除该目录下属于该账户的文件,不能修改该目录下其他账户创建的文件 |
3.7 文件长度
st_size保存文件的长度,write函数会修改该属性,也可以通过truncate修改文件大小,truncate可以扩大文件或者缩小文件,缩小文件时,文件内容会被删减。
文件大小可以通过ls
,wc -c
,stat
命令获取。
也可以通过fseek
和ftell
函数配合获取,或者直接通过stat
函数获取文件长度。
3.8 文件系统
3.8.1 文件管理
文件系统描述文件在硬盘中的组织,保存在硬盘中的文件只有普通文件、目录、软链接。
为了更加方便的管理持久性文件存储,操作系统一般对硬盘进行有规划的管理,规划包括:
文件系统指一个分区内,文件存储的组织方式。

在Linux下,通过mount命令将分区挂载到虚拟文件系统。
3.8.2 inode
一个硬盘分区,被格式化之后,可以认为硬盘被划分成两部分:管理数据和数据。管理数据部分保存着这个分区的分区信息,以及inode表。
inode保存文件的属性信息,stat命令能看到的信息,大部分都是保存在inode里的,一个inode占用128或者256字节,这个依赖具体的文件系统,每当在硬盘上创建一个文件/目录时,系统为这个文件/目录分配一个inode。值得注意的是,文件名,不存在inode中,而是存在文件所在目录的文件内容部分。
3.8.3 数据块
数据部分被简单的、按照等大尺寸的划分成n块,一般每块数据块的尺寸为1024-4096,由具体文件系统决定。
3.8.4 文件
当创建一个文件时,系统为该文件分配一个inode。如果往该文件写数据,那么系统为该文件分配数据块,inode会记录这个数据块位置,当一个数据块不够用时,系统会继续为它分配数据块。
3.8.5 目录
当创建一个目录时,系统为该目录分配一个inode,同时分配一个数据块,并且在该数据块中,记录文件.
和..
对应的inode。
如果在该目录下创建文件newfile
,那么参考上一节内容,会为该文件创建inode,最后将newfile
文件名和它的inode,作为一条记录,保存在目录的数据块中。

如果一个inode被别人引用,那么它的引用计数器会加1。
3.8.6 路径和寻址
Linux系统采用以/划分的路径字符串来寻址文件。
比如命令mkdir testdir
,寻址和操作过程如下图:

思考:为什么mv命令很快,而cp命令很慢,rename如何实现的
补充:分区
查看磁盘信息
sudo fdisk -l
磁盘名字 sda sdb ..
分区名字 sda1 sda2 ...
分区
sudo fdisk /dev/sdb
n 创建新分区
p 输出分区信息
w 保存分区信息并退出
分区和挂载
sudo mkfs.ext4 /dev/sdb1
sudo mount /dev/sdb1 xxyy
挂载成功之后,对xxyy目录的读写,其实是在/dev/sdb1文件系统中。
开机自动挂载
通过mount挂载的目录是临时的。如果希望开酒就挂载,那么可以将挂载命令写入到/etc/profile。或者修改/etc/fstab文件,/etc/fstab描述了开机需要挂载的文件系统信息。
去除挂载
通过手动umount去除挂载。
3.8.7 硬链接和软链接
硬链接不占用inode,只占用目录项。
软链接占用inode。
创建链接命令ln,硬链接只将对应的inode在目录总增加一个名字,并且将inode的引用计数器+1。
为了可以跨文件系统和对目录进行链接,创建了软链接这种方式。ln -s
// file --> file2
int main()
{
// get file2
struct stat buf;
stat("file", &buf);
// get 链接文件file的信息
struct stat buf2;
lstat("file", &buf2);
// 如果lstat的参数所指文件不是链接文件
// 那么它的效果和stat一样
struct stat buf3;
lstat("file2", &buf3)
}
读取symlink内容使用readlink命令。
删除软链接不会删除软链接指向的文件。
思考:为什么硬链接不能跨文件系统,而且不能对目录进行硬链接
3.8.8 虚拟文件系统VFS
内存无法加载硬盘所有内容,因为一般内存比硬盘小,但是在Linux内核中,维护了一个虚拟文件系统,将硬盘的目录结构映射到内存中。这个映射一般只包含已经被打开的文件。


3.9 文件删除
使用unlink命令和函数可以删除一个文件。
如果此时文件已经打开,那么该文件也可以被unlink,但是删除操作不会立即执行,而会被保留到文件关闭时执行。
unlink 删除文件,如果是链接,就删除链接,如果不是链接就删除文件。
rmdir 只能删除空目录
rm 会判断参数类型,如果是文件那么调用unlink,如果是目录调用rmdir
如果要删除非空目录,要使用rm -r,-r选项先删除目录中的文件,再调用rmdir。
int rm(const char* path);
{
// 怎么遍历目录
}
3.10 文件时间
对文件的访问,会导致文件时间发生变化。系统会自动记录用户对文件的操作的时间戳,以便将来可以查询文件修改时间。
如果需要故意修改,那么可以通过utime函数,修改文件的访问时间和修改时间。
touch
命令也可以将文件的时间修改为当前时间。touch
命令的副作用是,如果参数所指文件不存在,那么创建一个空文件。
当用户进行大规模拷贝时,cp
操作会修改文件的访问时间,如果想提高效率,可以使用-p
选项,避免文件属性的修改,同时加快速度。
#include <sys/types.h>
#include <utime.h>
int main()
{
struct utimbuf buf;
buf.actime = 0;
buf.modtime = 0;
utime("a.out", &buf);
}
利用utime来修改文件的访问时间和修改时间
#include "../h.h"
int main()
{
struct utimbuf buf;
buf.actime = 0;
buf.modtime = 0;
utime("testfile", &buf);
struct timeval tv[2];
tv[0].tv_sec = 100000;
tv[0].tv_usec = 10000;
tv[1].tv_sec = 100000;
tv[1].tv_usec = 10000;
utimes("testfile", tv);
}
3.11 目录操作
3.11.1 创建和删除目录
mkdir
和rmdir
3.11.2 遍历目录
opendir
,closedir
,readdir
,rewinddir
,telldir
,seekdir
遍历目录
#include "../h.h"
int rm(const char* path)
{
// 判断path是个文件还是目录
// 如果是文件,直接unlink然后返回
struct stat stat_buf;
int ret = stat(path, &stat_buf);
if(ret < 0)
{
perror("stat");
return -1;
}
if(!S_ISDIR(stat_buf.st_mode))
{
unlink(path);
return 0;
}
// 如果path是目录,遍历目录中的所有目录项
char buf[1024];
DIR* dir = opendir(path);
if(dir == NULL)
return -1;
struct dirent* entry = readdir(dir);
while(entry)
{
// 通过entry得到文件信息
sprintf(buf, "%s/%s", path, entry->d_name);
if(entry->d_type == DT_REG || entry->d_type == DT_LNK)
{
unlink(buf);
}
if(entry->d_type == DT_DIR)
{
// 忽略.和..目录
if(strcmp(entry->d_name, ".") == 0
||strcmp( entry->d_name, "..") == 0)
{
entry = readdir(dir);
continue;
}
rm(buf);
}
entry = readdir(dir);
}
closedir(dir);
rmdir(path);
return 0;
}
int main(int argc, char* argv[])
{
if(argc == 1)
{
printf("usage: %s [pathname]\n", argv[0]);
return 0;
}
rm(argv[1]);
return 0;
}
seekdir和telldir
#include "../h.h"
int main()
{
long loc;
DIR* dir = opendir("testdir");
// no
// seekdir(dir, 2);
//
struct dirent* entry;
while(1)
{
loc = telldir(dir);
entry = readdir(dir);
if(entry == NULL) break;
if(strcmp(entry->d_name, "a") == 0)
{
// 记录文件a的位置
break;
}
}
seekdir(dir, loc);
while(1)
{
entry = readdir(dir);
if(entry == NULL)
break;
printf("loc is %d, entry->d_name=%s\n", (int)telldir(dir), entry->d_name);
}
// 将文件指针回到a的位置
// seekdir(dir, loc);
}
#include <dirent.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
DIR* dir = opendir(argv[1]);
struct dirent* entry;
while(1)
{
entry = readdir(dir);
if(entry == NULL)
break;
// linux下,.开头是隐藏文件
if(entry->d_name[0] == '.')
continue;
printf("%s\n", entry->d_name);
}
closedir(dir);
}
3.12 练习
3.12.1 实现文件拷贝,保留文件属性
#include "../h.h"
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("usage: %s [srcfile] [dstfile]\n", argv[0]);
return -1;
}
const char* filesrc = argv[1];
const char* filedst = argv[2];
FILE* fp_src = fopen(filesrc, "r");
FILE* fp_dst = fopen(filedst, "w");
char buf[4096];
while(1)
{
int ret = fread(buf, 1, sizeof(buf), fp_src);
if(ret <= 0)
break;
fwrite(buf, ret, 1, fp_dst);
}
fclose(fp_src);
fclose(fp_dst);
// 获取源文件属性
struct stat src_stat;
stat(filesrc, &src_stat);
// 修改目标文件时间
struct utimbuf timbuf;
timbuf.actime = src_stat.st_atime;
timbuf.modtime = src_stat.st_mtime;
utime(filedst, &timbuf);
// 修改权限
chmod(filedst, src_stat.st_mode);
// 修改所有者
int ret = chown(filedst, src_stat.st_uid, src_stat.st_gid);
if(ret < 0)
{
perror("chown");
}
return 0;
}
3.12.2 实现目录打包到文件,将文件解包成目录
#include "../h.h"
#include <map>
#include <string>
using namespace std;
map<ino_t, std::string> savedfiles;
void tarfile(const char* filename, FILE* fpOut)
{
struct stat stat_buf;
stat(filename, &stat_buf);
// 检查这个文件是否已经保存过,是否是其他文件的硬链接
std::string filesaved = savedfiles[stat_buf.st_ino];
if(filesaved.size() != 0)
{
// 此ino之前已经存过了
fprintf(fpOut, "h\n%s\n%s\n", filename, filesaved.c_str());
return;
}
fprintf(fpOut, "f\n%s\n%lld\n", filename, (long long int)stat_buf.st_size);
FILE* fpIn = fopen(filename, "r");
char buf[4096];
while(1)
{
int ret = fread(buf, 1, sizeof(buf), fpIn);
if(ret <= 0)
break;
fwrite(buf, ret, 1, fpOut);
}
fclose(fpIn);
// savedfiles.insert(std::pair<ino_t, std::string>(stat_buf.st_ino, std::string(filename)));
// 如果该文件不是别人的硬链接,那么将文件拷贝之后,在全局map中记录ino
savedfiles[stat_buf.st_ino] = std::string(filename);
}
int tardir(const char* dirname, FILE* fpOut)
{
char filepath[1024];
// 写目录
fprintf(fpOut, "d\n");
fprintf(fpOut, "%s\n", dirname);
DIR* dir = opendir(dirname);
struct dirent* entry = readdir(dir);
while(entry)
{
sprintf(filepath, "%s/%s", dirname, entry->d_name);
// handle
if(entry->d_type == DT_REG)
{
// write file
tarfile(filepath, fpOut);
}
else if(entry->d_type == DT_DIR)
{
if(strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
{
entry = readdir(dir);
continue;
}
tardir(filepath, fpOut);
}
entry = readdir(dir);
}
closedir(dir);
}
int tar(const char* dirname, const char* outfile)
{
FILE* fpOut = fopen(outfile, "w");
fprintf(fpOut, "xgltar\n");
fprintf(fpOut, "1.0\n");
int ret = tardir(dirname, fpOut);
fclose(fpOut);
return ret;
}
#define line_buf_size 1024
char line_buf[line_buf_size];
#define get_line() fgets(buf, line_buf_size, fin)
int untar1(FILE* fin)
{
char* buf = line_buf;
if(get_line() == NULL)
return -1;
printf("now utar type=%s", buf);
if(strcmp(buf, "d\n") == 0)
{
get_line();
buf[strlen(buf)-1] = 0;
mkdir(buf, 0777);
printf("mkdir %s\n", buf);
}
else if(strcmp(buf, "f\n") == 0)
{
get_line();
buf[strlen(buf)-1] = 0; // filename
FILE* out = fopen(buf, "w");
printf("create file %s\n", buf);
get_line();
long long int len = atoll(buf); // 1987\n
printf("filelen %s\n", buf);
while(len > 0)
{
// 避免粘包问题
int readlen = len < line_buf_size ? len : line_buf_size;
int ret = fread(buf, 1, readlen, fin);
fwrite(buf, 1, ret, out);
len -= ret;
}
fclose(out);
}
else if(strcmp(buf, "h\n") == 0)
{
get_line();
buf[strlen(buf)-1] = 0; // filename new
std::string new_path(buf);
get_line();
buf[strlen(buf)-1] = 0; // hardline filename old
link(buf, new_path.c_str());
}
return 0;
}
int untar(const char* tarfile)
{
char* buf = line_buf;
FILE* fin = fopen(tarfile, "r");
//fgets(buf, line_buf_size, fin);
get_line();
if(strcmp(buf, "xgltar\n") != 0)
{
printf("unknown file format\n");
return -1;
}
get_line();
if(strcmp(buf, "1.0\n") == 0)
{
while(1)
{
int ret = untar1(fin);
if(ret != 0)
break;
}
}
else
{
printf("unknown version\n");
return -1;
}
return 0;
}
// ./mytar -c dir tarfile.xgl
// ./mytar -u tarfile.xgl
int main(int argc, char* argv[])
{
if(argc == 1)
{
printf("usage: \n\t%s -c [dir] [tarfile]\n\t%s -u [tarfile]\n", argv[0], argv[0]);
return -1;
}
const char* option = argv[1];
if(strcmp(option, "-c") == 0)
{
const char* dirname = argv[2];
const char* outfile = argv[3];
return tar(dirname, outfile);
}
else if(strcmp(option, "-u") == 0)
{
const char* tarfile = argv[2];
return untar(tarfile);
}
printf("option error\n");
return -1;
}
3.13 函数和命令
3.13.1 函数
stat/lstat:查看文件属性
chmod:修改文件权限
chown:修改文件的所有者
utime:修改文件时间
unlink:删除文件
link:创建硬链接
symlink:创建软链接
rmdir:删除空目录
mkdir:创建空目录
opendir:打开目录
closedir:关闭目录
readdir:读取一个目录项,并且将目录项指针移到下一项
seekdir:修改目录项指针
rewainddir:重置目录项指针
telldir:获得当前目录向指针
判断权限位宏 S_IRUSR(stat.st_mode)
判断文件类型宏S_ISDIR(stat.st_mode)
3.13.2 命令
stat:查看文件属性
chmod:修改文件权限
chown
unlink:删除文件(不会跟随)
ln:创建链接
mkdir
rmdir
rm
cp
dd:拷贝数据(可以拷贝文件,也拷贝块设备)
wc:计算文件内容的行数、单词数、字节数
which:查找非内置的命令位置
fdisk:查看磁盘信息、分区
mkfs:在分区中创建文件系统(ext2,ext3,ext4, fat32, ntfs, xfs,nfs)
mount:挂载
umount:取消挂载