目录
一、虚拟地址空间
1、虚拟地址空间简介
2、为什么要有虚拟地址空间
直接使用物理内存带来的问题
如何解决
3、虚拟地址空间的分区
二、文件描述符
1、文件描述符简介
2、文件描述符表
文件描述符表总结
虚拟地址空间是操作系统给进程描述的一个虚假的地址空间。
虚拟地址空间的大小由操作系统决定,32位的操作系统虚拟地址空间的大小为 2^32 字节,也就是 4G;64 位的操作系统虚拟地址空间大小为 2^64 字节。
当我们运行磁盘上一个可执行程序, 就会得到一个进程,内核会给每一个运行的进程创建一块属于自己的虚拟地址空间,并将应用程序数据装载到虚拟地址空间对应的地址上。
进程在运行过程中,程序内部所有的指令都是通过 CPU 处理完成的,CPU 只进行数据运算并不具备数据存储的能力,其处理的数据都加载自物理内存,那么进程中的数据是如何进出入到物理内存中的呢?其实是通过 CPU 中的内存管理单元 MMU(Memory Management Unit)从进程的虚拟地址空间中映射过去的。
为什么操作系统不直接将数据加载到物理内存中?
而是将数据加载到虚拟地址空间中,在通过 CPU 的 MMU 映射到物理内存中呢?
如果直接将数据加载到物理内存会发生什么事情:
假设计算机的物理内存大小为 1G。
- 进程 A 需要 100M 内存,因此直接在物理内存上从 0 地址开始分配 100M;
- 进程 B 启动需要 250M 内存,因此继续在物理内存上为其分配 250M 内存;
- 并且进程 A 和进程 B 占用的内存是连续的;
- 之后再启动其他进程继续按照这种方法进行物理内存的分配。。
这种分配方式带来的问题:
由于程序都是直接访问物理内存,所以恶意程序可以通过内存寻址随意修改别的进程对应的内存数据,以达到破坏的目的。虽然有些时候是非恶意的,但是有些存在 bug 的程序可能不小心修改了其它程序的内存数据,就会导致其它程序的运行出现异常。
如果直接使用物理内存的话,一个进程对应的内存块就是作为一个整体操作的,如果出现物理内存不够用的时候,我们一般的办法是将不常用的进程拷贝到磁盘的交换分区(虚拟内存)中,以便腾出内存,因此就需要将整个进程一起拷走,如果数据量大,在内存和磁盘之间拷贝时间就会很长,效率低下。
由于物理内存的使用情况一直在动态的变化,我们无法确定内存现在使用到哪里了,如果直接将程序数据加载到物理内存,内存中每次存储数据的起始地址都是不一样的,这样数据的加载都需要使用相对地址,加载效率低(静态库是使用绝对地址加载的)。
有了虚拟地址空间之后就可以完美的解决上边提到的所有问题了,虚拟地址空间就是一个中间层,相当于在程序和物理内存之间设置了一个屏障,将二者隔离开来。程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映射,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠,就可以达到内存地址空间隔离的效果。
虚拟地址空间主要分为两个部分,分别是 内核区和用户区。
下图是进程对应的虚拟地址空间的各个分区 (以 32 位系统的虚拟地址空间为例)。
每个进程的虚拟地址空间都是从 0 地址开始的,我们在程序中打印的变量地址也其在虚拟地址空间中的地址,程序是无法直接访问物理内存的。
虚拟地址空间中用户区地址范围是 0~3G,里边分为多个区块:
在 Linux 操作系统中的一切都被抽象成了文件,那么一个打开的文件是如何与应用程序进行对应呢?
解决方案是使用文件描述符(file descriptor,简称fd),当在进程中打开一个现有文件或者创建一个新文件时,内核向该进程返回一个文件描述符,用于对应这个打开/新建的文件。这些文件描述符都存储在内核为每个进程维护的一个文件描述符表中。
在 Linux 系统中一切皆文件,系统中一切都被抽象成了文件。对这些文件的读写都需要通过文件描述符来完成。标准 C 库的文件 IO 函数使用的文件指针 FILE* ,在 Linux 中也需要通过文件描述符的辅助才能完成读写操作。FILE 其实是一个结构体,其内部有一个成员就是文件描述符。
FILE 结构体在 Linux 头文件中的定义:
// linux c FILE结构体定义: /usr/include/libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; // 文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
// 在文件: /usr/include/stdio.h
typedef struct _IO_FILE FILE;
启动一个进程就会得到一个对应的虚拟地址空间,这个虚拟地址空间分为两大部分,在内核区有专门用于进程管理的模块。
Linux 的进程控制块 PCB(process control block)本质是一个叫做 task_struct 的结构体,里边包括管理进程所需的各种信息,其中有一个结构体叫做 file ,我们将它叫做文件描述符表,里边有一个整形索引表,用于存储文件描述符。
内核为每一个进程维护了一个文件描述符表,索引表中的值都是从 0 开始的,所以在不同的进程中你会看到相同的文件描述符,但是它们指向的不一定是同一个磁盘文件。
Linux 中用户操作的每个终端都被视作一个设备文件,当前操作的终端文件可以使用 /dev/tty 表示。
当一个进程被启动之后,内核 PCB 的文件描述符表中就已经分配了三个文件描述符,这三个文件描述符对应的都是当前启动这个进程的终端文件(Linux 中一切皆文件,终端就是一个设备文件,在 /dev 目录中)
STDIN_FILENO:标准输入,可以通过这个文件描述符将数据输入到终端文件中,宏值为 0。
STDOUT_FILENO:标准输出,可以通过这个文件描述符将数据通过终端输出出来,宏值为 1。
STDERR_FILENO:标准错误,可以通过这个文件描述符将错误信息通过终端输出出来,宏值为 2。
这三个默认分配的文件描述符是可以通过 close() 函数关闭掉,但是关闭之后当前进程也就不能和当前终端进行输入或者输出的信息交互了。
通过 open() 函数打开 /hello.txt,文件描述符 3 被分配给了这个文件,保持这个打开状态,再次通过 open() 函数打开 /hello.txt,文件描述符 4 被分配给了这个文件,也就是说一个进程中不同的文件描述符打开的磁盘文件可能是同一个。
通过 open() 函数打开 /hello.txt,文件描述符 3 被分配给了这个文件,将打开的文件关闭,此时文件描述符 3 就被释放了。再次通过 open() 函数打开 /hello.txt,文件描述符 3 被分配给了这个文件,也就是说打开的新文件会关联文件描述符表中最小的没有被占用的文件描述符。
通过 open 函数我们即可打开一个磁盘文件,如果磁盘文件不存在还可以创建一个新的的文件,函数原型如下:
#include
#include
#include
/*
open是一个系统函数, 只能在linux系统中使用, windows不支持
fopen 是标准c库函数, 一般都可以跨平台使用, 可以这样理解:
- 在linux中 fopen底层封装了Linux的系统API open
- 在window中, fopen底层封装的是 window 的 api
*/
// 打开一个已经存在的磁盘文件
int open(const char *pathname, int flags);
// 打开磁盘文件, 如果文件不存在, 就会自动创建
int open(const char *pathname, int flags, mode_t mode);
通过 open 函数可以让内核给文件分配一个文件描述符,如果需要释放这个文件描述符就需要关闭文件。对应的这个系统函数叫做 close,函数原型如下:
#include
int close(int fd); // fd 是文件描述符,是 open () 函数的返回值
// 函数调用成功返回值 0, 调用失败返回 -1
read 函数用于读取文件内部数据,在通过 open 打开文件的时候需要指定读权限,函数原型如下:
#include
ssize_t read(int fd, void *buf, size_t count);
// 参数含义:
// fd: 文件描述符,open () 函数的返回值,通过这个参数定位打开的磁盘文件
// buf: 是一个传出参数,指向一块有效的内存,用于存储从文件中读出的数据
// count: buf 指针指向的内存的大小,指定可以存储的最大字节数
//返回值:
//大于 0: 从文件中读出的字节数,读文件成功
//等于 0: 代表文件读完了,读文件成功
// -1: 读文件失败了
write 函数用于将数据写入到文件内部,在通过 open 打开文件的时候需要指定写权限,函数原型如下:
#include
ssize_t write(int fd, const void *buf, size_t count);
//参数
//fd: 文件描述符,open () 函数的返回值,通过这个参数定位打开的磁盘文件
//buf: 指向一块有效的内存地址,里边有要写入到磁盘文件中的数据
//count: 要往磁盘文件中写入的字节数,一般情况下就是 buf 字符串的长度,strlen (buf)
//返回值:
//大于 0: 成功写入到磁盘文件中的字节数
//-1: 写文件失败了
// 文件的拷贝
#include
#include
#include
#include
#include
int main()
{
// 1. 打开存在的文件english.txt, 读这个文件
int fd1 = open("./english.txt", O_RDONLY);
if(fd1 == -1)
{
perror("open-readfile");
return -1;
}
// 2. 打开不存在的文件, 将其创建出来, 将从english.txt读出的内容写入这个文件中
int fd2 = open("copy.txt", O_WRONLY|O_CREAT, 0664);
if(fd2 == -1)
{
perror("open-writefile");
return -1;
}
// 3. 循环读文件, 循环写文件
char buf[4096];
int len = -1;
while( (len = read(fd1, buf, sizeof(buf))) > 0 )
{
// 将读到的数据写入到另一个文件中
write(fd2, buf, len);
}
// 4. 关闭文件
close(fd1);
close(fd2);
return 0;
}
系统函数 lseek 的功能是比较强大的,我们既可以通过这个函数移动文件指针,也可以通过这个函数进行文件的拓展。这个函数的原型如下:
#include
#include
off_t lseek(int fd, off_t offset, int whence);
//参数
//fd: 文件描述符,open () 函数的返回值,通过这个参数定位打开的磁盘文件
//offset: 偏移量,需要和第三个参数配合使用
//whence: 通过这个参数指定函数实现什么样的功能
//SEEK_SET: 从文件头部开始偏移 offset 个字节
//SEEK_CUR: 从当前文件指针的位置向后偏移 offset 个字节
//SEEK_END: 从文件尾部向后偏移 offset 个字节
//返回值
//成功:文件指针从头部开始计算总的偏移量
//失败: -1
lseek(fd, 0, SEEK_SET); // 文件指针移动到文件头部
lseek(fd, 0, SEEK_CUR); // 得到当前文件指针的位置
lseek(fd, 0, SEEK_END); // 得到文件总大小
假设使用一个下载软件进行一个大文件下载,但是磁盘很紧张,如果不能马上将文件下载到本地,磁盘空间就可能被其他文件占用了,导致下载软件下载的文件无处存放。那么这个文件怎么解决呢?
我们可以在开始下载的时候先进行文件拓展,将一些字符写入到目标文件中,让拓展的文件和即将被下载的文件一样大,这样磁盘空间就被成功抢到手,软件就可以慢悠悠的下载对应的文件了。
使用 lseek 函数进行文件拓展必须要满足一下条件:
// lseek.c
// 拓展文件大小
#include
#include
#include
int main()
{
int fd = open("hello.txt", O_RDWR);
if(fd == -1)
{
perror("open");
return -1;
}
// 文件拓展, 一共增加了 1001 个字节
lseek(fd, 1000, SEEK_END);
write(fd, " ", 1);
close(fd);
return 0;
}
truncate/ftruncate 这两个函数的功能是一样的,可以对文件进行拓展也可以截断文件。使用这两个函数拓展文件比使用 lseek 要简单。这两个函数的函数原型如下:
// 拓展文件或截断文件
#include
#include
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
//参数
//path: 要拓展 或 截断的文件的文件名
//fd: 文件描述符,open () 得到的
//length: 文件的最终大小
//文件原来 size > length,文件被截断,尾部多余的部分被删除,文件最终长度为 length
//文件原来 size < length,文件被拓展,文件最终长度为 length
//返回值:成功返回 0; 失败返回值 - 1
在查看 Linux 系统函数的时候,我们可以发现一个规律:大部分系统函数的返回值都是整形,并且通过这个返回值来描述系统函数的状态(调用是否成功了)。在 man 文档中关于系统函数的返回值大部分时候都是这样描述的:
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set
appropriately.
如果成功,则返回0。出现错误时,返回-1,并给errno设置一个适当的值。
errno 是一个全局变量,只要调用的 Linux 系统函数有异常(返回 - 1), 错误对应的错误号就会被设置给这个全局变量。这个错误号存储在系统的两个头文件中:
- /usr/include/asm-generic/errno-base.h
- /usr/include/asm-generic/errno.h
得到错误号,去查询对应的头文件是非常不方便的,我们可以通过 perror 函数将错误号对应的描述信息打印出来:
#include
// 参数, 自己指定这个字符串的值就可以, 指定什么就会原样输出, 除此之外还会输出错误号对应的描述信息
void perror(const char *s);
// open.c
#include
#include
#include
int main()
{
int fd = open("hello.txt", O_RDWR|O_EXCL|O_CREAT, 0777);
if(fd == -1)
{
perror("open");
return -1;
}
close(fd);
return 0;
}
如果想要查看某一个文件的属性有两种方式:命令和函数。
该命令用来识别文件类型,也可用来辨别一些文件的编码格式。它是通过查看文件的头部信息来获取文件类型,而不是像 Windows 通过扩展名来确定文件类型的。
# 参数在命令中的位置没有限制
$ file 文件名 [参数]
# file 命令的参数是可选项,可以不加,常用的参数如下表:
-b 只显示文件类型和文件编码,不显示文件名
-i 显示文件的 MIME 类型
-F 设置输出字符串的分隔符
-L 查看软连接文件自身文件属性
使用不带任何选项的 file 命令,即可查看指定文件的类型和文件编码信息。
# 空文件
$ file 11.txt
11.txt: empty
# 源文件, 编码格式为: ASCII
$ file b.cpp
b.cpp: C source, ASCII text
# 源文件, 编码格式为: UTF-8
robin@OS:~$ file test.cpp
test.cpp: C source, UTF-8 Unicode (with BOM) text, with CRLF line terminators
# 可执行程序, Linux中的可执行程序为 ELF 格式
robin@OS:~$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=5317ae9fba592bf583c4f680d8cc48a8b58c96a5, not stripped
使用 -b 参数,可以使 file 命令的输出不出现文件名,只显示文件格式以及编码。
# 空文件
$ file 11.txt -b
empty
# 源文件, 编码格式为: ASCII
$ file b.cpp -b
C source, ASCII text
# 源文件, 编码格式为: UTF-8
robin@OS:~$ file test.cpp -b
C source, UTF-8 Unicode (with BOM) text, with CRLF line terminators
# 可执行程序, Linux中的可执行程序为 ELF 格式
robin@OS:~$ file a.out -b
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=5317ae9fba592bf583c4f680d8cc48a8b58c96a5, not stripped
MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。
是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
# charset 为该文件的字符编码
# 源文件, MIME类型: text/x-c, 字符编码: utf-8
$ file occi.cpp -i
occi.cpp: text/x-c; charset=utf-8
# 压缩文件, MIME类型: application/gzip, 字符编码: binary
$ file fcgi.tar.gz -i
fcgi.tar.gz: application/gzip; charset=binary
# 文本文件, MIME类型: text/plain, 字符编码: utf-8
$ file english.txt -i
english.txt: text/plain; charset=utf-8
# html文件, MIME类型: text/html, 字符编码: us-ascii
$ file demo.html -i
demo.html: text/html; charset=us-ascii
在 file 命令中,文件名和后边的属性信息默认使用冒号(:)分隔,我们可以通过 -F 参数修改分隔符,分隔符可以是单字符也可以是一个字符串,如果分隔符是字符串需要将这个参数值写到引号中(单 / 双引号都可以)。
# 默认格式输出
$ file english.txt
english.txt: UTF-8 Unicode text, with very long lines, with CRLF line terminators
# 修改分隔符为字符串 “==>"
$ file english.txt -F "==>"
english.txt==> UTF-8 Unicode text, with very long lines, with CRLF line terminators
修改分隔符为单字符 '='
$ file english.txt -F =
english.txt= UTF-8 Unicode text, with very long lines, with CRLF line terminators
直接通过 file 查看文件属性得到的是链接文件指向的文件的信息,如果添加参数 -L 得到的链接文件自身的属性信息。
stat 命令显示文件或目录的详细属性信息包括文件系统状态,比 ls 命令输出的信息更详细。
# 参数在命令中的位置没有限制
$ stat [参数] 文件或者目录名
参数 功能
-f 不显示文件本身的信息,显示文件所在文件系统的信息
-L 查看软链接文件关联的文件的属性信息。使用 stat 查看软链接类型的文件,默认显示的是这个软链接文件的属性信息
-c 查看文件某个单个的属性信息
-t 简洁模式,只显示摘要信息,不显示属性描述
stat/lstat 函数的功能和 stat 命令的功能是一样的,只不过是应用场景不同。这两个函数的区别在于处理软链接文件的方式上:
#include
#include
#include
int stat(const char *pathname, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
pathname: 文件名,要获取这个文件的属性信息
buf: 传出参数,文件的信息被写入到了这块内存中
返回值:函数调用成功返回 0,调用失败返回 -1
这个函数的第二个参数是一个结构体类型,这个结构体相对复杂,通过这个结构体可以存储得到的文件的所有属性信息,结构体原型如下:
struct stat {
dev_t st_dev; // 文件的设备编号
ino_t st_ino; // inode节点
mode_t st_mode; // 文件的类型和存取的权限, 16位整形数 -> 常用
nlink_t st_nlink; // 连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
dev_t st_rdev; // (设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; // 文件字节数(文件大小) --> 常用
blksize_t st_blksize; // 块大小(文件系统的I/O 缓冲区大小)
blkcnt_t st_blocks; // block的块数
time_t st_atime; // 最后一次访问时间
time_t st_mtime; // 最后一次修改时间(文件内容)
time_t st_ctime; // 最后一次改变时间(指属性)
};
#include
int main()
{
// 1. 定义结构体, 存储文件信息
struct stat myst;
// 2. 获取文件属性 english.txt
int ret = stat("./english.txt", &myst);
if(ret == -1)
{
perror("stat");
return -1;
}
printf("文件大小: %d\n", (int)myst.st_size);
return 0;
}
文件的类型信息存储在 struct stat 结构体的 st_mode 成员中,它是一个 mode_t 类型,本质上是一个 16 位的整数。Linux API 中为我们提供了相关的宏函数,通过对应的宏函数可以直接判断出文件是不是某种类型。
// 类型是存储在结构体的这个成员中: mode_t st_mode;
// 这些宏函数中的m 对应的就是结构体成员 st_mode
// 宏函数返回值: 是对应的类型返回-> 1, 不是对应类型返回0
S_ISREG(m) is it a regular file?
- 普通文件
S_ISDIR(m) directory?
- 目录
S_ISCHR(m) character device?
- 字符设备
S_ISBLK(m) block device?
- 块设备
S_ISFIFO(m) FIFO (named pipe)?
- 管道
S_ISLNK(m) symbolic link? (Not in POSIX.1-1996.)
- 软连接
S_ISSOCK(m) socket? (Not in POSIX.1-1996.)
- 本地套接字文件
int main()
{
// 1. 定义结构体, 存储文件信息
struct stat myst;
// 2. 获取文件属性 english.txt
int ret = stat("./hello", &myst);
if(ret == -1)
{
perror("stat");
return -1;
}
printf("文件大小: %d\n", (int)myst.st_size);
// 判断文件类型
if(S_ISREG(myst.st_mode))
{
printf("这个文件是一个普通文件...\n");
}
if(S_ISDIR(myst.st_mode))
{
printf("这个文件是一个目录...\n");
}
if(S_ISLNK(myst.st_mode))
{
printf("这个文件是一个软连接文件...\n");
}
return 0;
}
用户对文件的操作权限也存储在 struct stat 结构体的 st_mode 成员中,在这个 16 位的整数中不同用户的权限存储位置如下图,如果想知道有没有相关权限可以通过按位与 (&) 操作将这个标志位值取出判断即可。
Linux 中为我们提供了用于不同用户不同权限判定使用的宏,具体信息如下:
关于变量 st_mode:
- st_mode -- 16位整数
○ 0-2 bit -- 其他人权限
- S_IROTH 00004 读权限 100
- S_IWOTH 00002 写权限 010
- S_IXOTH 00001 执行权限 001
- S_IRWXO 00007 掩码, 过滤 st_mode中除其他人权限以外的信息
○ 3-5 bit -- 所属组权限
- S_IRGRP 00040 读权限
- S_IWGRP 00020 写权限
- S_IXGRP 00010 执行权限
- S_IRWXG 00070 掩码, 过滤 st_mode中除所属组权限以外的信息
○ 6-8 bit -- 文件所有者权限
- S_IRUSR 00400 读权限
- S_IWUSR 00200 写权限
- S_IXUSR 00100 执行权限
- S_IRWXU 00700 掩码, 过滤 st_mode中除文件所有者权限以外的信息
○ 12-15 bit -- 文件类型
- S_IFSOCK 0140000 套接字
- S_IFLNK 0120000 符号链接(软链接)
- S_IFREG 0100000 普通文件
- S_IFBLK 0060000 块设备
- S_IFDIR 0040000 目录
- S_IFCHR 0020000 字符设备
- S_IFIFO 0010000 管道
- S_IFMT 0170000 掩码,过滤 st_mode中除文件类型以外的信息
############### 按位与操作举例 ###############
1111 1111 1111 1011 # st_mode
0000 0000 0000 0100 # S_IROTH
&
----------------------------------------
0000 0000 0000 0000 # 没有任何权限
#include
int main()
{
// 1. 定义结构体, 存储文件信息
struct stat myst;
// 2. 获取文件属性 english.txt
int ret = stat("./hello", &myst);
if(ret == -1)
{
perror("stat");
return -1;
}
printf("文件大小: %d\n", (int)myst.st_size);
// 判断文件类型
if(S_ISREG(myst.st_mode))
{
printf("这个文件是一个普通文件...\n");
}
if(S_ISDIR(myst.st_mode))
{
printf("这个文件是一个目录...\n");
}
if(S_ISLNK(myst.st_mode))
{
printf("这个文件是一个软连接文件...\n");
}
// 文件所有者对文件的操作权限
printf("文件所有者对文件的操作权限: ");
if(myst.st_mode & S_IRUSR) //件所有者权限,R是读
{
printf("r");
}
if(myst.st_mode & S_IWUSR)
{
printf("w");
}
if(myst.st_mode & S_IXUSR)
{
printf("x");
}
printf("\n");
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
if(argc < 2)
{
printf("./a.out filename\n");
exit(1);
}
struct stat st;
int ret = stat(argv[1], &st); //获得文件属性
if(ret == -1)
{
perror("stat");
exit(1);
}
// 存储文件类型和访问权限
char perms[11] = {0};
// 判断文件类型
switch(st.st_mode & S_IFMT)//12-15 bit文件类型,S_IFMT,过滤 st_mode中除文件类型以外的信息
{
case S_IFLNK: //符号链接(软链接)
perms[0] = 'l';
break;
case S_IFDIR: //目录
perms[0] = 'd';
break;
case S_IFREG: //普通文件
perms[0] = '-';
break;
case S_IFBLK: //块设备
perms[0] = 'b';
break;
case S_IFCHR: //字符设备
perms[0] = 'c';
break;
case S_IFSOCK: //套接字文件
perms[0] = 's';
break;
case S_IFIFO: //管道
perms[0] = 'p';
break;
default:
perms[0] = '?';
break;
}
// 判断文件的访问权限
// 文件所有者
perms[1] = (st.st_mode & S_IRUSR) ? 'r' : '-';
perms[2] = (st.st_mode & S_IWUSR) ? 'w' : '-';
perms[3] = (st.st_mode & S_IXUSR) ? 'x' : '-';
// 文件所属组
perms[4] = (st.st_mode & S_IRGRP) ? 'r' : '-';
perms[5] = (st.st_mode & S_IWGRP) ? 'w' : '-';
perms[6] = (st.st_mode & S_IXGRP) ? 'x' : '-';
// 其他人
perms[7] = (st.st_mode & S_IROTH) ? 'r' : '-';
perms[8] = (st.st_mode & S_IWOTH) ? 'w' : '-';
perms[9] = (st.st_mode & S_IXOTH) ? 'x' : '-';
// 硬链接计数
int linkNum = st.st_nlink;
// 文件所有者
char* fileUser = getpwuid(st.st_uid)->pw_name;
// 文件所属组
char* fileGrp = getgrgid(st.st_gid)->gr_name;
// 文件大小
int fileSize = (int)st.st_size;
// 修改时间
char* time = ctime(&st.st_mtime);
char mtime[512] = {0};
strncpy(mtime, time, strlen(time)-1);
char buf[1024];
sprintf(buf, "%s %d %s %s %d %s %s",
perms, linkNum, fileUser, fileGrp, fileSize, mtime, argv[1]);
printf("%s\n", buf);
return 0;
}
在 Linux 中只要调用 open() 函数就可以给被操作的文件分配一个文件描述符,除了使用这种方式 Linux 系统还提供了一些其他的 API 用于文件描述符的分配,相关函数有三个:dup, dup2, fcntl。
dup 函数的作用是复制文件描述符,这样就有多个文件描述符可以指向同一个文件了。函数原型如下:
#include
int dup(int oldfd);
// oldfd 是要被复制的文件描述符
// 函数调用成功返回被复制出的文件描述符,调用失败返回 -1
被复制出的新文件描述符是独立于旧的文件描述符的,二者没有连带关系。也就是说当旧的文件描述符被关闭了,复制出的新文件描述符还是可以继续使用的。
#include
#include
#include
#include
#include
int main()
{
// 1. 创建一个新的磁盘文件
int fd = open("./mytest.txt", O_RDWR|O_CREAT, 0664);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);
// 写数据
const char* pt = "你好, 世界......";
// 写成功之后, 文件指针在文件尾部
write(fd, pt, strlen(pt));
// 复制这个文件描述符 fd
int newfd = dup(fd);
printf("newfd: %d\n", newfd);
// 关闭旧的文件描述符
close(fd);
// 使用新的文件描述符继续写文件
const char* ppt = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(newfd, ppt, strlen(ppt));
close(newfd);
return 0;
}
dup2 () 函数是 dup () 函数的加强版,基于 dup2 () 既可以进行文件描述符的复制,也可以进行文件描述符的重定向。文件描述符重定向就是改变已经分配的文件描述符关联的磁盘文件。
#include
// 1. 文件描述符的复制, 和dup是一样的
// 2. 能够重定向文件描述符
// - 重定向: 改变文件描述符和文件的关联关系, 和新的文件建立关联关系, 和原来的文件断开关联关系
// 1. 首先通过open()打开文件 a.txt , 得到文件描述符 fd
// 2. 然后通过open()打开文件 b.txt , 得到文件描述符 fd1
// 3. 将fd1重定向 到fd上:
// fd1和b.txt这磁盘文件断开关联, 关联到a.txt上, 以后fd和fd1都对用同一个磁盘文件 a.txt
int dup2(int oldfd, int newfd);
// oldfd 和 newfd 都是文件描述符
// 函数调用成功返回新的文件描述符,调用失败返回 -1
// 使用dup2 复制文件描述符
#include
#include
#include
#include
#include
void fun()
{
// 1. 创建一个新的磁盘文件
int fd = open("./111.txt", O_RDWR|O_CREAT, 0664);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);
// 写数据
const char* pt = "你好, 世界......";
// 写成功之后, 文件指针在文件尾部
write(fd, pt, strlen(pt));
// 2. fd1没有对应任何的磁盘文件, fd1 必须要 >=0
int fd1 = 1023;
// fd -> 111.txt
// 文件描述符复制, fd1指向fd对应的文件 111.txt
dup2(fd, fd1);
// 关闭旧的文件描述符
close(fd);
// 使用fd1写文件
const char* ppt = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(fd1, ppt, strlen(ppt));
close(fd1);
return 0;
}
// 使用dup2 文件描述符重定向
int main()
{
// 1. 创建一个新的磁盘文件
int fd = open("./111.txt", O_RDWR|O_CREAT, 0664);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);
// 写数据
const char* pt = "你好, 世界......";
// 写成功之后, 文件指针在文件尾部
write(fd, pt, strlen(pt));
// 2. 创建第二个磁盘文件 222.txt
int fd1 = open("./222.txt", O_RDWR|O_CREAT, 0664);
if(fd1 == -1)
{
perror("open1");
exit(0);
}
// fd -> 111.txt, fd1->222.txt
// 从定向, 将fd1指向fd对应的文件 111.txt
dup2(fd, fd1);
// 关闭旧的文件描述符
close(fd);
// 使用fd1写文件
const char* ppt = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(fd1, ppt, strlen(ppt));
close(fd1);
return 0;
}
fcntl () 是一个变参函数,并且是多功能函数,在这里只介绍如何通过这个函数实现文件描述符的复制和获取/设置已打开的文件属性。该函数的函数原型如下:
#include
#include // 主要的头文件
int fcntl(int fd, int cmd, ... /* arg */ );
// fd: 要操作的文件描述符
// cmd: 通过该参数控制函数要实现什么功能
// cmd = F_DUPFD:返回新的 被分配的 文件描述符(复制一个已经存在的文件描述符)
// cmd = F_GETFL:返回文件的 flag 属性信息
// cmd=F_SETFL: 设置文件的状态标志
// 返回值:函数调用失败返回 -1,调用成功,返回正确的值:
// 文件的状态标志指的是在使用 open () 函数打开文件的时候指定的 flags 属性,也就是第二个参数
// int open(const char *pathname, int flags);
使用 fcntl () 复制文件描述符,函数返回值为新分配的文件描述符:
#include
#include
#include
#include
#include
int main()
{
// 1. 创建一个新的磁盘文件
int fd = open("./mytest.txt", O_RDWR|O_CREAT, 0664);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);
// 写数据
const char* pt = "你好, 世界......";
// 写成功之后, 文件指针在文件尾部
write(fd, pt, strlen(pt));
// 复制这个文件描述符 fd
int newfd = fcntl(fd, F_DUPFD);
printf("newfd: %d\n", newfd);
// 关闭旧的文件描述符
close(fd);
// 使用新的文件描述符继续写文件
const char* ppt = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(newfd, ppt, strlen(ppt));
close(newfd);
return 0;
}
设置文件状态标志:
通过 open() 函数打开文件之后,文件的 flag 属性就已经被确定下来了,如果想要在打开状态下修改这些属性,可以使用 fcntl() 函数实现,但是有一点需要注意,不是所有的 flag 属性都能被动态修改,只能修改如下状态标志: O_APPEND, O_NONBLOCK, O_SYNC, O_ASYNC, O_RSYNC 等。
// 得到文件的flag属性
int flag = fcntl(fd, F_GETFL);
// 添加新的flag 标志
flag = flag | O_APPEND;
// 将更新后的falg设置给文件
fcntl(fd, F_SETFL, flag);
使用场景:果要往当前文件中写数据,打开一个新文件,文件的写指针在文件头部,数据默认也是写到文件开头,如果不想将数据写到文件头部,可以给文件追加一个 O_APPEND 属性。
// 写实例程序, 给文件描述符追加 O_APPEND
#include
#include
#include
#include
#include
int main()
{
// 1. 打开一个已经存在的磁盘文件
int fd = open("./111.txt", O_RDWR);
if(fd == -1)
{
perror("open");
exit(0);
}
printf("fd: %d\n", fd);
// 如果不想将数据写到文件头部, 可以给文件描述符追加一个O_APPEND属性
// 通过fcntl获取文件描述符的 flag属性
int flag = fcntl(fd, F_GETFL);
// 给得到的flag追加 O_APPEND属性
flag = flag | O_APPEND; // flag |= O_APPEND;
// 重新将flag属性设置给文件描述符
fcntl(fd, F_SETFL, flag);
// 使用fd写文件, 添加的数据应该写到文件尾部
const char* ppp = "((((((((((((((((((((((骚年,你要相信光!!!))))))))))))))))))))))";
write(fd, ppp, strlen(ppp));
close(fd);
return 0;
}
在目录操作之前必须要先通过 opendir () 函数打开这个目录。
#include
#include
// 打开目录
DIR *opendir(const char *name);
参数: name -> 要打开的目录的名字
返回值: DIR*, 结构体类型指针。打开成功返回目录的实例,打开失败返回 NULL
目录打开之后,就可以通过 readdir () 函数遍历目录中的文件信息了。每调用一次这个函数就可以得到目录中的一个文件信息,当目录中的文件信息被全部遍历完毕会得到一个空对象。
// 读目录
#include
struct dirent *readdir(DIR *dirp);
参数:dirp -> opendir () 函数的返回值
返回值:函数调用成功,返回读到的文件的信息,目录文件被读完了或者函数调用失败返回 NULL
函数返回值 struct dirent 结构体原型如下:
struct dirent {
ino_t d_ino; /* 文件对应的inode编号, 定位文件存储在磁盘的那个数据块上 */
off_t d_off; /* 文件在当前目录中的偏移量 */
unsigned short d_reclen; /* 文件名字的实际长度 */
unsigned char d_type; /* 文件的类型, linux中有7中文件类型 */
char d_name[256]; /* 文件的名字 */
};
关于结构体中的文件类型 d_type,可使用的宏值如下:
DT_BLK:块设备文件
DT_CHR:字符设备文件
DT_DIR:目录文件
DT_FIFO :管道文件
DT_LNK:软连接文件
DT_REG :普通文件
DT_SOCK:本地套接字文件
DT_UNKNOWN:无法识别的文件类型
// 关闭目录, 参数是 opendir() 的返回值
int closedir(DIR *dirp);
参数:dirp-> opendir () 函数的返回值
返回值:目录关闭成功返回 0, 失败返回 -1
如果只遍历单层目录是不需要递归的,设我们需要得到某个指定目录下 mp3 格式文件的个数:
// filenum.c
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
// 1. 打开目录
DIR* dir = opendir(argv[1]);
if(dir == NULL)
{
perror("opendir");
return -1;
}
// 2. 遍历当前目录中的文件
int count = 0;
while(1)
{
struct dirent* ptr = readdir(dir);
if(ptr == NULL)
{
printf("目录读完了...\n");
break;
}
// 读到了一个文件
// 判断文件类型
if(ptr->d_type == DT_REG)
{
//在字符串ptr->d_name 中查找第一次出现字符串".mp3" 的位置,不包含终止符 '\0'。
char* p = strstr(ptr->d_name, ".mp3");
if(p != NULL && *(p+4) == '\0')
{
count++;
printf("file %d: %s\n", count, ptr->d_name);
}
}
}
printf("%s目录中mp3文件的个数: %d\n", argv[1], count);
// 关闭目录
closedir(dir);
return 0;
}
// filenum.c
#include
#include
#include
#include
#include
int getMp3Num(const char* path)
{
// 1. 打开目录
DIR* dir = opendir(path);
if(dir == NULL)
{
perror("opendir");
return 0;
}
// 2. 遍历当前目录
struct dirent* ptr = NULL;
int count = 0;
while((ptr = readdir(dir)) != NULL)
{
// 如果是目录 . .. 跳过不处理
if(strcmp(ptr->d_name, ".")==0 ||
strcmp(ptr->d_name, "..") == 0)
{
continue;
}
// 假设读到的当前文件是目录
if(ptr->d_type == DT_DIR)
{
// 目录
char newPath[1024];
sprintf(newPath, "%s/%s", path, ptr->d_name);
// 读当前目录的子目录
count += getMp3Num(newPath);
}
else if(ptr->d_type == DT_REG)
{
// 普通文件
char* p = strstr(ptr->d_name, ".mp3");
// 判断文件后缀是不是 .mp3
if(p != NULL && *(p+4) == '\0')
{
count++;
printf("%s/%s\n", path, ptr->d_name);
}
}
}
closedir(dir);
return count;
}
int main(int argc, char* argv[])
{
// ./a.out path
if(argc < 2)
{
printf("./a.out path\n");
return 0;
}
int num = getMp3Num(argv[1]);
printf("%s 目录中mp3文件个数: %d\n", argv[1], num);
return 0;
}
除了使用上边介绍的目录三剑客遍历目录,也可以使用 scandir() 函数进行目录的遍历(只遍历指定目录,不进入到子目录中进行递归遍历),它的参数并不简单,涉及到三级指针和回调函数的使用。
// 头文件
#include
int scandir(const char *dirp,
struct dirent ***namelist,
int (*filter)(const struct dirent *),
int (*compar)(const struct dirent **, const struct dirent **) );
int alphasort(const struct dirent **a, const struct dirent **b);
int versionsort(const struct dirent **a, const struct dirent **b);
dirp: 需要遍历的目录的名字
namelist: 三级指针,传出参数,需要在指向的地址中存储遍历目录得到的所有文件的信息
在函数内部会给这个指针指向的地址分配内存,要注意在程序中释放内存
filter: 函数指针,指针指向的函数就是回调函数,需要在自定义函数中指定如果过滤目录中的文件
如果不对目录中的文件进行过滤, 该函数指针指定为NULL即可
如果自己指定过滤函数, 满足条件要返回1, 否则返回 0
compar: 函数指针,对过滤得到的文件进行排序,可以使用提供的两种排序方式:
alphasort: 根据文件名进行排序
versionsort: 根据版本进行排序
返回值:函数执行成功返回找到的匹配成功的文件的个数,如果失败返回 - 1。
scandir() 可以让使用者自定义文件的过滤方式,然后将过滤函数的地址传递给 scandir () 的第三个参数:
// 函数的参数就是遍历的目录中的子文件对应的结构体
int (*filter)(const struct dirent *);
int isMp3(const struct dirent *ptr)
{
if(ptr->d_type == DT_REG)
{
char* p = strstr(ptr->d_name, ".mp3");
if(p != NULL && *(p+4) == '\0')
{
return 1;
}
}
return 0;
}
#include
#include
#include
#include
#include
// 文件过滤函数
int isMp3(const struct dirent *ptr)
{
if(ptr->d_type == DT_REG)
{
char* p = strstr(ptr->d_name, ".mp3");
if(p != NULL && *(p+4) == '\0')
{
return 1;
}
}
return 0;
}
int main(int argc, char* argv[])
{
if(argc < 2)
{
printf("./a.out path\n");
return 0;
}
struct dirent **namelist = NULL;
int num = scandir(argv[1], &namelist, isMp3, alphasort);
for(int i=0; id_name);
free(namelist[i]);
}
free(namelist);
return 0;
}
struct dirent **namelist 指向的什么类型的数据?
答案:指向的是一个指针数组 struct dirent *namelist[]