我们可以把一个磁盘分成一个或多个分区,每个分区可以包含一个文件系统。下图是一个磁盘分区格式化成ext2文件系统后的存储布局。
- 文件系统中存储的最小单位是块(Block),一个块究竟多大是在格式化时确定的,例如mke2fs的-b选项可以设定块大小为1024、2048或4096字节。而上图中启动块(BootBlock)的大小是确定的,就是1KB,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。
- 启动块之后才是ext2文件系统的开始,ext2文件系统将整个分区划成若干个同样大小的块组(Block Group),每个块组都由以下部分组成。
点击查看关于启动块的介绍和管理的文章
数据块(Data Block) 根据不同的文件类型有以下几种情况
inode中存着许多inode,每个inode占128B,每个inode存放着文件属性和数据块指针,数据块指针呢占60B。下图表示如何根据inode找到数据块。
inode进行寻址有两种方式:
- 直接寻址,也就是inode记录了数据存放的数据块的编号。
- 间接寻址,间接寻址又分成一级间接寻址,二级间接寻址,三级间接寻址。以一集间接寻址为例,inode记录的是一个数据块编号,这个数据块不存放数据,而是分成若干个(数据块大小/4B个)记录,每个记录存放数据块的编号,这些数据块存放数据。通过这么一级一级的扩大数据存放范围。
举一个例子,假如一个数据块Block大小为4KB,每个数据块编号为4B,那么每个Block就可以寻址4KB/4B = 1K个数据块。
这样,一个inode就可以寻址一个48KB+4MB+4GB+4TB大小的文件,已经足够了。
#include
#include
#include
int stat(const char *pathname, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
//返回值:若成功,返回0;若出错,返回-1
这三个函数有一个相同类型的参数struct stat *类型的buf,buf是一个指针,指向这么一个结构
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
struct timespec st_atim; /* time of last access */
struct timespec st_mtim; /* time of last modification */
struct timespec st_ctim; /* time of last status change */
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
这些文件类型信息包含在stat结构的st_mode成员中,
宏 | 文件类型 |
---|---|
S_ISLNK(m) | 符号链接 |
S_ISREG(m) | 普通文件 |
S_ISDIR(m) | 目录文件 |
S_ISCHR(m) | 字符特殊文件 |
S_ISBLK(m) | 块特殊文件 |
S_ISFIFO(m) | 管道或FIFO |
S_ISSOCK(m) | 套接字 |
例如S_ISLNK(m)的宏定义为:
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
每一个文件都有一个所有者和所有组,所有者由stat结构中的st_uid指定,所有组则由st_gid指定。
通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。
当执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,有效组ID就是实际组ID。但是,文件的模式字中设置一个特殊标志,例如passwd命令,它执行的文件是/usr/bin/passwd这个文件,这个文件设置了特殊标志
menwen@menwen:~/$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 53128 3月 29 2016 /usr/bin/passwd
这个s权限的意思是:当执行此文件时,将进程的有效用户ID设置成为文件所有者的用户ID。相当于以root用户来执行该文件。在文件模式字中的这一位就是设置用户ID位,设置组ID位就是所有组上的”s”标志位。
每个文件有9个访问权限位,可以分成三类。
st_mode屏蔽 | 含义 |
---|---|
S_IRUSR | 用户读 |
S_IWUSR | 用户写 |
S_IXUSR | 用户执行 |
S_IRGRP | 组读 |
S_IWGRP | 组写 |
S_IXGRP | 组执行 |
S_IROTH | 其他读 |
S_IWOTH | 其他写 |
S_IXOTH | 其他执行 |
chamod(1)命令用于修改这9个权限位。u表示所有者,g表示组,o表示其他。
#include
int access(const char *pathname, int mode);
//返回值:若成功,返回0,若出错,返回-1
mode的说明
mode | 说明 |
---|---|
R _OK | 测试读权限 |
W_OK | 测试写权限 |
X_OK | 测试执行权限 |
F_OK | 测试文件是否存在 |
内核测试的步骤:(按照下面的顺序)
1. 若进程的有效用户ID是0(超级用户),则允许访问。
2. 若进程的有效用户ID等于文件按所有者ID,那么如果所有者适当的访问权限被设置,则允许访问。意思是:如果进程要进行写操作,那么就检查用户写位是否为1。
3. 若进程的有效组ID或附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问。
4. 若其他用户适当的访问权限位被设置,则允许访问。
注意:如果进程拥有此文件,则按照用户访问权限批准或拒绝该进程对文件的访问——不查看组访问权限,其他类似。
access函数的实例
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
if(argc < 2){
printf("./aut filename\n");
exit(1);
}
if(access(argv[1], F_OK) < 0){
perror("access");
exit(1);
}else{
printf("%s : file exist\n", argv[1]);
}
if(access(argv[1], R_OK) < 0){
perror("access error");
}else{
printf("read access OK\n");
}
if(open(argv[1], O_RDONLY) < 0){
perror("open error");
exit(1);
}else{
printf("open read OK\n");
}
return 0;
}
下面是程序的实例会话:
menwen@menwen:~/$ ls -l /etc/shadow
-rw-r----- 1 root shadow 1344 10月 10 20:28 /etc/shadow
menwen@menwen:~/$ ls -l a.out
-rwsrwxr-x 1 root menwen 7532 10月 27 19:52 a.out
menwen@menwen:~/$ ./a.out /etc/shadow
/etc/shadow : file exist
access error: Permission denied
open error: Permission denied
当将a.out程序文件的所有者改为root,并打开设置用户的ID(加s标志)
sudo chown root a.out
sudo chmod u+s a.out
menwen@menwen:~/APUE_code/FILE_DIR$ ./a.out /etc/shadow
/etc/shadow : file exist
access error: Permission denied
open read OK
虽然可以open函数打开,但是设置用户ID程序可以确定实际用户不能正常读指定的文件。
函数umask可以为进程设置文件模式创建屏蔽字,并返回之前的值。
#include
#include
mode_t umask(mode_t mask);
//返回值:之前的文件模式创建的屏蔽字
cmask参数就是标题5—-文件访问权限的9个常量中的若干个按位“或”构成的。
umask程序实例
#include
#include
#include
#define RWRWRW (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)
int main()
{
umask(0);
if(open("foo", O_CREAT | O_RDONLY, RWRWRW) < 0){
perror("open foo err");
}
umask(S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
if(open("bar", O_RDONLY | O_CREAT, RWRWRW) < 0){
perror("open bar err");
}
return 0;
}
运行结果如下:
menwen@menwen:~/$ ls -l bar foo
-------rw- 1 menwen menwen 0 10月 27 20:22 bar
-rw-rw-rw- 1 menwen menwen 0 10月 27 20:22 foo
这俩个函数可以是我们更改现有的文件的访问权限
#include
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
//返回值:若成功,返回0,若出错,返回-1
mode参数除了之前的9个,还有6个如下
mode | 说明 |
---|---|
S_ISUID | 执行时设置用户ID |
S_ISGID | 执行时设置组ID |
S_ISVTX | 保存正文(粘着位) |
S_IRWXU | 所有者读、写、执行 |
S_IRWXG | 组读、写、执行 |
S_IRWXO | 其他读、写、执行 |
chmod函数的实例:
#include
#include
#include
int main()
{
struct stat buf;
if(stat("foo", &buf) < 0){
perror("stat err");
exit(1);
}
if(chmod("foo", (buf.st_mode & ~S_IXGRP) | S_ISGID) < 0){
perror("chmod foo err");
exit(1);
}
return 0;
}
运行结果:
menwen@menwen:~/$ ls -l foo
-rw-rwSrw- 1 menwen menwen 0 10月 27 20:22 foo
注意:
关于粘着位
drwxrwxrwt 16 root root 4096 10月 27 21:01 tmp
可以更改文件的用户ID和组ID,如果连个owner或者group中的任意一个是-1,则对应的ID不变。
#include
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *pathname, uid_t owner, gid_t group);
//返回值:若成功,返回0;若出错,返回0
chmod函数的实例:
#include
#include
#include
#include
int main(int argc, char *argv[])
{
struct stat buf;
if(stat(argv[1], &buf) < 0){
perror("stat err");
exit(1);
}
printf("uid = %d\tgid = %d\n", buf.st_uid, buf.st_gid);
if(chown(argv[1], 0, 0) < 0){//所有者和所有组改为root
perror("chown err");
exit(1);
}
if(stat(argv[1], &buf) < 0){
perror("stat err");
}
printf("uid = %d\tgid = %d\n", buf.st_uid, buf.st_gid);
return 0;
}
将程序改所有者为root,且打开设置用户ID位
sudo chown root a.out
sudo chmod u+s a.out
menwen@menwen:~/$ ll a.out
-rwsrwxr-x 1 root menwen 7584 10月 27 22:30 a.out*
menwen@menwen:~/$ ll file
-rw-rw-r-- 1 menwen menwen 0 10月 27 22:36 file
menwen@menwen:~/$ ./a.out file
uid = 1000 gid = 1000
uid = 0 gid = 0
menwen@menwen:~/$ ll file
-rw-rw-r-- 1 root root 0 10月 27 22:36 file
stat结构成员st_size表示以字节位单位的文件长度。此字段只对普通文件、目录文件和符号链接有意义。
linux系统提供了st_blksize和st_blocks两个字段。
空洞是由所设置的偏移量超过文件尾端,并写入一些数据后造成的。
实例:
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int n;
struct stat buf;//文件信息的缓冲区
int fd = open(argv[1], O_WRONLY | O_CREAT, 0664);//创建一个文件
if(fstat(fd, &buf) < 0){
perror("fstat err");
exit(1);
}
printf("blksize = %ld\tblocks = %ld\n", buf.st_blksize, buf.st_blocks);
//打印出st_blksize和st_blocks
if(fd < 0){
perror("open err");
exit(1);
}
lseek(fd, 0x1000, SEEK_SET);//偏移4096个字节
if(write(fd, "test", 4) < 0){//写一个"test"
perror("write err");
exit(1);
}
close(fd);
fd = open(argv[1], O_WRONLY);
if(fstat(fd, &buf) < 0){
perror("fstat err");
exit(1);
}
printf("blksize = %ld\tblocks = %ld\n", buf.st_blksize, buf.st_blocks);
//打印写成文件的st_blksize和st_blocks
close(fd);
return 0;
}
运行结果如下:
menwen@menwen:~/$ ./a.out file.core
blksize = 4096 blocks = 0
blksize = 4096 blocks = 8
menwen@menwen:~/$ ls -l file.core
-rw-rw-r-- 1 menwen menwen 4100 10月 28 15:58 file.core
menwen@menwen:~/$ du -s file.core
4 file.core
menwen@menwen:~/$ wc -c file.core
4100 file.core
根据打印出的blksize和blocks可以得知;
根据du -s、ls -l、wc -c这三个命令可以看出:
文件长度位4100字节,但du命令报告该文件所使用的磁盘空间总量为4个512字节块,所以文件中有空洞。
如果复制一个空洞文件,那么空洞文件会被填满,其中的所有实际数据字节皆填写为0.
menwen@menwen:~/$ cat file.core > file.copy
menwen@menwen:~/$ ls -l file*
-rw-rw-r-- 1 menwen menwen 4100 10月 28 16:20 file.copy
-rw-rw-r-- 1 menwen menwen 4100 10月 28 16:19 file.core
menwen@menwen:~/$ du -s file*
8 file.copy
4 file.core
打开文件时使用O_TRUNC标志可以将文件截断位0
函数truncate和ftruncate可以截断文件
#include
#include
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
//返回值:若成功,返回0,若出错,返回-1
函数实例:
#include
#include
#include
#include
int main(int argc, char *argv[])
{
if(truncate(argv[1], 500) < 0){//把文件截断到500字节
perror("truncate err");
exit(1);
}
return 0;
}
运行结果:
menwen@menwen:~/$ ll file
-rw-rw-r-- 1 menwen menwen 1000 10月 28 16:37 file
menwen@menwen:~/$ ./a.out file
menwen@menwen:~/$ ll file
-rw-rw-r-- 1 menwen menwen 500 10月 28 16:39 file
使用link函数创建一个指向现有文件的链接
#include
int link(const char *oldpath, const char *newpath);
//返回值:若成功,返回0, 若出错,返回-1
#include
#include
#include
#include
int main()
{
if(link("file", "file.link") < 0){
perror("link err");
exit(1);
}
}
运行结果:
menwen@menwen:~/$ df .
文件系统 1K-块 已用 可用 已用% 挂载点
/dev/sda1 18447100 6422224 11064776 37% /
menwen@menwen:~/$ ./a.out
done
menwen@menwen:~/$ ll file file.link
-rwxrwxr-x 2 menwen menwen 7492 10月 29 11:47 file*
-rwxrwxr-x 2 menwen menwen 7492 10月 29 11:47 file.link*
menwen@menwen:~/$ df .
文件系统 1K-块 已用 可用 已用% 挂载点
/dev/sda1 18447100 6422224 11064776 37% /
从结果可以看出,调用link函数位file文件创建了一个file.link的硬链接,硬链接为2,并且创建应链接前后,磁盘的可用的空间大小没有变化。这是因为:在数据块存放着许多文件内容,而目录也是文件,它在数据块中只存放的是目录中的目录项的名字和inode编号,file和file.link同为一个目录下的文件,两者的inode存放着同一个inode编号,而inode一直存放着这两个文件的内容在数据块中的地址。所以磁盘可用大小没有变化。
为了删除一个现有的目录项
#include
int unlink(const char *pathname);
//返回值:若成功,返回0,若出错,返回-1
int main()
{
int fd;
if((fd = open("file.link", O_RDONLY)) < 0){//打开file.link文件
perror("open file.link err");
exit(1);
}
printf("open file.link\n");
if(unlink("file.link") < 0){//解除file.link
perror("unlink file.link err");
exit(1);
}
if(unlink("file") < 0){//解除file
perror("unlink file.link err");
exit(1);
}
sleep(10);
printf("done\n");
close(fd);
}
运行结果:
menwen@menwen:~/$ ./a.out &
[1] 20703
menwen@menwen:~/$ open file.link
df .
文件系统 1K-块 已用 可用 已用% 挂载点
/dev/sda1 18447100 6422224 11064776 37% /
menwen@menwen:~/$ done
df .
文件系统 1K-块 已用 可用 已用% 挂载点
/dev/sda1 18447100 6422216 11064784 37% /
[1]+ 已完成 ./a.out
menwen@menwen:~/$ ll file file.link
ls: 无法访问'file': 没有那个文件或目录
ls: 无法访问'file.link': 没有那个文件或目录
让程序在后台运行,检查空间没有变化,是因为程序打开了file.link文件,当程序运行完毕后,对比前后磁盘空间变化,解除链接后磁盘可用空间增大8K空间。
unlink函数一般解除文件链接,rmdir函数解除目录链接,而用remove函数解除一个文件或目录的链接,
#include
int remove(const char *pathname);
//返回值:若成功,返回0,若出错,返回-1
使用rename函数对文件重命名
#include
int rename(const char *oldpath, const char *newpath);
//返回值:若成功,返回0,若出错,返回-1
符号链接是一个文件的间接引用,与硬链接不同,硬链接直接指向文件inode,符号链接可以避开硬链接的一些限制。
医用用symlink函数创建一个符号链接
#include
int symlink(const char *target, const char *linkpath);
//返回值:若成功,返回0,若出错,返回-1
函数创建一个指向target的新目录项linkpath,在创建此符号链接时,不要求target已经存在,并且target和linkpath并不要位于同一文件系统中。
因为open函数跟随符号链接,所以需要用readlink函数打开链接,读链接中的名字。
#include
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
//返回值:若成功,返回读取的字节数,若出错,返回-1
readlink函数包含了open、read、close的所有操作,将内容读到buf,并且不以null结尾。
struct stat结构中timespec结构类型按照秒和纳秒定义了时间。
struct timespec {
long ts_sec;
long ts_nsec;
};
提供了更高精度的时间戳,为了保持兼容性,因此可以用st_atime定义成st_atime.tv_sec。
关于st_atim,st_mtim,st_ctim辨析
- st_atim是最近一次访问(access)文件的时间,如read
- st_mtim是最近一次修改(modification)文件的时间,如write
- st_ctim是最近一次更改inode的时间,如chmod、chown
调用utimes函数可以更新inode的时间
#include
int utimes(const char *filename, const struct timeval times[2])
//返回值:若成功,返回读取的字节数,若出错,返回-1
#include
#include
int mkdir(const char *pathname, mode_t mode);
//返回值:若成功,返回0, 若出错返回-1
mkdir函数可以创建新的目录,其中“.”和“..”自动创建。所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。目录至少要设置一个执行权限位。
#include
int rmdir(const char *pathname);
//返回值:若成功,返回0, 若出错返回-1
rmdir函数删除一个空目录。空目录只有“.”和“..”这两项目录
若调用这两个函数时目录链接计数为0,并且也没有其他进程打开此目录,则释放由此目录占用的空间。
只有内核才能写目录,一个目录有写和执行权限决定了在该目录下可以新建文件以及删除文件,不代表能否写目录本身。
#include
#include
DIR *opendir(const char *name);
DIR *fdopendir(int fd);
//返回值:若成功,返回指针,若出错,返回NULL
struct dirent *readdir(DIR *dirp);
//返回值:若成功,返回指针,若出差或在目录尾,返回NULL
void rewinddir(DIR *dirp);
int closedir(DIR *dirp);
//返回值:若成功,返回0,若出错返回-1
long telldir(DIR *dirp);
//返回值:与drip关联的目录中的当前位置
void seekdir(DIR *dirp, long loc);
一般由opendir和fopendir返回指向DIR结构的指针由另外5个函数调用。
调用chdir和fchdir函数可以更改当前工作目录
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点。
/etc/passwd中用户登录的第六个字段是用户的起始目录,也就是家目录。当前工作目录是进程的一个属性,起始目录是登录名的一个属性。
#include
int chdir(const char *path);
int fchdir(int fd);
这两个函数分别用path或打开文件描述符来指定当前工作目录。
chdir是跟随符号链接。
因为当前工作目录是进程的一个属性,所以它只影响调用chdir的进程本身,而不影响其他进程。
#include
char *getcwd(char *buf, size_t size);
//返回值:若成功,返回buf;若出错,返回NULL
getcwd函数可以得到完整的绝对路径名,有两个参数,一个是缓冲区地址buf,一个是缓冲区的长度size。缓冲区有足够的长度以容纳绝对路径名加上一个终止null字节。
linux系统最长文件名为255字节。
#include
#include
#include
#define MAXSIZE 4096
int main()
{
char buf[MAXSIZE];
if(chdir("/") < 0){//改变目录
perror("chdir err");
exit(1);
}
printf("pwd = %s\n", getcwd(buf, sizeof(buf)));//打印出绝对路径
return 0;
}
运行结果:
menwen@menwen:~/$ pwd
/home/menwen/APUE_code/FILE_DIR
menwen@menwen:~/$ ./a.out
pwd = /
menwen@menwen:~/$ pwd
/home/menwen/APUE_code/FILE_DIR
该程序并没有改变当前shell的工作目录,因为他俩个是独立的进程。
getcwd可以实现一个应用程序返回它工作的出发点,通过保存更换目录前的绝对路径。fchdir也通过返回打开更换目录之前目录的文件描述符保存路径。