open/close函数
查看open函数帮助手册:
- 终端:
man 2 open
(按G跳转到末尾,一般会有小demo) - vim:光标移到函数处,命令模式下输入2K(2代表系统调用)
头文件可简化为:#include<unistd.h>
和#include <fcntl.h>
open函数返回值为文件描述符,三个参数分别为:文件路径、打开模式(可以通过“|”运算符来组合多个flag)和权限(仅适用于创建新文件,并且权限受umask影响:文件权限 = mode & ~umask)
举例如下:
第三个例子的参数含义:如果a.cp文件存在,则清空为0且以只读方式打开;若不存在,则创建a.cp文件且赋予644权限,即rw-r–r–
close函数返回值为0表示成功,-1 表示有错误发生,参数为文件描述符
errno
即error number,相当于内核中的全局变量,不用自己定义,只需引入头文件#include<errno.h>
,用来帮助我们确认错误信息,比如:
open函数常见错误:
- 打开文件不存在(即上述举例情况)
- 以写方式打开只读文件
- 以只写方式打开目录
笔记:
错误处理函数
- 直接输出errno:
printf("xxx error: %d\n", errno);
- strerror函数:
char *strerror(int errnum)
,示例:printf("xxx error: %s\n", strerror(errno));
perror
函数:void perror(const char *s)
,参数是自定义的错误信息前缀。示例:perror("open errorrrr!");
打开不存在的文件,输出如下:
read函数
ssize_t read(int fd, void *buf, size_t count);
参数:
- fd:文件描述符
- buf:存数据的缓冲区
- count:缓冲区大小
返回值:实际读到的字节数
- 0:读到文件末尾
- >0:成功,读到的字节数
- -1:失败,设置 errno(如果errno = EAGIN 或 EWOULDBLOCK,说明不是read失败,而是read在以非阻塞方式读一个设备文件/网络文件,并且文件无数据)
ps:可以用perror
来提示错误👇
write函数
ssize_t write(int fd, const void *buf, size_t count);
参数:
- fd:文件描述符
- buf:待写出数据的缓冲区(与read函数相比多了const关键字,目的是防止写函数过程中误操作把原文件修改了)
- count:数据大小
返回值:
- 成功:写入的字节数
- 失败: -1, 设置 errno
预读入缓输出机制
像read、write这类函数通常被称为unbuffered I/O,指的是无用户级缓冲区(图中蓝色缓冲区),但不保证不使用内核缓冲区。read、write系统函数使用用户级缓冲区要自己定义,而fputc这类标准库函数自带用户级缓冲区,一般默认大小为4096字节。
缓冲区
- unbuffered I/O(系统调用)无用户级缓冲区
- 标准库函数自带用户级缓冲区
- 内核自带缓冲区
- 缓冲区默认都是4kb,即4096字节
系统调用
是什么:由操作系统实现并提供给外部应用程序的编程接口,是应用程序和系统之间数据交互的桥梁。
C标准函数和系统函数调用关系,一个helloworld如何打印到屏幕上:
库函数和系统调用
两种函数的执行过程:
- 执行库函数–》系统调用–》调用内核驱动
- 执行系统函数–》直接调用内核驱动
看似好像系统函数比库函数执行起来少了一步,速度会更快,但并不一定。一是因为标准库函数沉淀了数年,相较于自己写的函数,优化工作已经做得很好;二是在不清楚原理的前提下,直接使用系统函数,可能发挥不出原本的性能。例如上述的预读入缓输出机制,fputc函数自带用户级缓冲区,而write函数要自己定义,如果定义的缓冲区大小不合适,写入的速度在大部分情况下就不如fputc函数。
因此,在编程时尽量使用标准库函数。
文件描述符
进程地址空间如下图所示,以0-4G举例:0-3G是用户区,3G-4G是内核区,write这种系统调用可以完成用户区到内核区的切换。
PCB进程控制块:本质是结构体,有很多成员,其中一个很重要的成员就是文件描述符表。表中的每一个元素就是一个个文件描述符,表面是int整数(即fd,图中右侧的下标,特性是新打开文件时优先使用数字最小的空闲下标),但本质是指向一个文件结构体的指针。文件结构体即struct file1{···}
,里面记录了该文件的一些信息,比如文件的偏移量、访问权限等等。
如上图文件描述符表所示,一个进程最多打开的文件数量为1024,其中前三个是固定的:
- 0 – STDIN_FILENO(对应终端输入设备)
- 1 – STDOUT_FILENO
- 2 – STDERR_FILENO
文件描述符:0/1/2/3/4。。。。/1023 表中可用的最小的。
阻塞/非阻塞
产生阻塞的场景:读设备文件、读网络文件(读常规文件无阻塞概念)
阻塞/非阻塞是设备、网络文件的属性,不是导致阻塞的函数(比如read系统函数)的属性
示例:终端设备文件/dev/tty默认为阻塞状态,这意味着如果打开时文件无数据就会一直阻塞,解决方法为以非阻塞方式打开,这样读取时如果无数据会直接返回-1
open("/dev/tty", O_RDWR|O_NONBLOCK)
fcntl函数
int fcntl(int fd, int cmd, ... /* arg */ );
功能(之一):改变一个已经打开的文件的访问控制属性
- 获取文件属性: F_GETFL
- 设置文件属性: F_SETFL
示例:上个示例通过重新打开终端设备文件/dev/tty利用open函数来设置其为非阻塞状态,但也可以不用非得重新打开文件来设置其为非阻塞状态,而是通过fcntl函数
int flgs = fcntl(fd, F_GETFL); //获取文件状态
flgs |= O_NONBLOCK; //对非阻塞标志进行或等于位运算
fcntl(fd, F_SETFL, flgs); //设置文件状态为非阻塞
lseek函数
off_t lseek(int fd, off_t offset, int whence);
参数:
- fd:文件描述符
- offset:偏移量
- whence:起始偏移位置: SEEK_SET、SEEK_CUR、SEEK_END
返回值:
- 成功:较起始位置偏移量
- 失败:-1 errno
应用场景:
- 文件的“读”、“写”操作共同一偏移位置,因此如果写完想再读出来就需要写完把偏移位置往前移
- 使用lseek获取文件大小
int length = lseek(fd, 0, SEEK_END);
- 使用lseek拓展文件大小,但是要想使文件大小真正拓展,必须有IO操作
ps:使用lseek函数来获取文件大小并不常规,一般使用stat函数。同样,使用lseek函数来拓展文件大小也不常规,一般使用truncate函数直接拓展文件:int ret = truncate("dict.cp", 250);
返回值为0表示拓展成功,-1表示失败
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
传入/传出参数
传入参数
- 指针作为函数参数
- 通常有const关键字修饰
- 指针指向有效区域, 在函数内部做读操作
示例:char *strcpy(char *dest, const char *src);
的第二个参数const char *src
传出参数
- 指针作为函数参数
- 在函数调用之前,指针指向的空间可以无意义,但必须有效
- 在函数内部,做写操作
- 函数调用结束后,充当函数返回值
示例:char *strcpy(char *dest, const char *src);
的第一个参数char *dest
传入传出参数
- 指针作为函数参数
- 在函数调用之前,指针指向的空间有实际意义
- 在函数内部,先做读操作,后做写操作
- 函数调用结束后,充当函数返回值
示例:char *strtok_r(char *str, const char *delim, char **saveptr);
的第三个参数char **saveptr
文件存储
Inode
概念:表面是一个整数(inode号),本质上是结构体,存储文件的属性信息。比如:权限、类型、大小、时间、用户、盘块位置等等,大多数的inode都存储在磁盘上,少量常用、近期使用的inode会被缓存到内存中。
dentry
概念:目录项,其本质依然是结构体,重要成员变量有两个:文件名和inode号,文件内容保存在磁盘盘块中。当对一个文件创建硬链接时,也就是创建了一个新的目录项。
stat/lstat 函数
功能:获取文件属性(从inode结构体中获取)
头文件: #include <sys/stat.h>
函数原型:int stat(const char *pathname, struct stat *buf);
返回值:
- 0:成功
- -1:失败 errno
参数:
- path:文件路径(文件名)
- buf:存放文件属性(传出参数,也是一个结构体,
struct stat
是一个在Linux系统中用于描述文件状态的结构体)- buf.st_size:文件大小
- buf.st_mode:文件类型
- buf.st_nlink:硬链接数
- buf.st_mode:文件权限(文件权限位图如下图所示)
lstat函数与stat函数基本一致,但是在判断文件类型时,stat会穿透符号链接(软链接),而lstat不会(即abc是一个目录,abc.s是它的软链接,用stat函数判断abc.s文件类型结果是目录,而用lstat函数判断abc.s文件类型结果是软链接)
link/unlink函数
函数原型:
int link(const char *oldpath, const char *newpath);
int unlink(const char *pathname);
Linux系统删除文件的机制是不断将硬链接 -1,直至减到0为止。无目录项对应的文件,将会被操作系统择机释放(具体时间由系统内部调度算法决定)。因此从某种意义上说,删除文件只是让文件具备了被释放的条件。
unlink函数的特征:清除文件时,如果文件的硬链接数到0了,没有entry对应,但该文件仍不会马上被释放。要等到所有打开该文件的进程关闭该文件,系统才会挑时间将该文件释放掉。
当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放,系统的这一特性被称为隐式回收系统资源。
目录操作函数
目录文件也是“文件”,其文件内容是该目录下所有子文件的目录项 dentry,目录文件的rwx权限与普通文件有所区别:
- r读权限:对于目录而言就是目录可以被浏览,比如ls、tree命令
- w写权限:对于目录而言就是目录文件内容,也就是目录项可以被增删修改,即目录里的文件可以被增删修改,比如mv、touch、mkdir命令
- x执行权限:对于目录而言就是目录可以被打开、进入,比如cd命令。
opendir函数
功能:根据传入的目录名打开一个目录(库函数,所以手册是第三卷:man 3 opendir
)
函数原型:DIR *opendir(const char *name);
返回值为目录结构体的指针
closedir函数
功能:关闭打开的目录
函数原型:int closedir(DIR *dirp);
readdir函数
功能:读取目录(库函数)
函数原型:struct dirent *readdir(DIR *dirp);
返回值为目录项结构体的指针:
struct dirent {
inode;
char dname[256];
···
};
ps:一般要配合while循环使用,因为一次只能读取一个目录项
编程练习:实现递归遍历目录(类似于ls -R
)
思路:
- 判断命令行参数,获取用户要查询的目录名
- 目录名:
argv[1]
- 如果用户没有传入目录,当作
./
处理,判断条件:argc == 1
- 目录名:
- 判断用户指定的是否是目录:
stat S_ISDIR();
–> 封装函数isFile() { }
- 读目录伪代码:
read_dir() {
opendir(dir)
while(readdir()) {
普通文件:直接打印
目录:
拼接目录访问绝对路径:sprintf(path, "%s/%s", dir, d_name)
递归调用自己→ opendir(path) readdir closedir
}
closedir()
}
read_dir() --> isFile() ---> read_dir()
完整代码:my_ls-R.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<dirent.h>
void read_dir(char *dir);
void isFile(char *name);
//处理目录
void read_dir(char *dir) {
DIR *dp;
struct dirent *sdp;
char path[256];
dp = opendir(dir);
if(dp == NULL) {
perror("opendir error:");
return ;
}
//读取目录项
while((sdp = readdir(dp)) != NULL) {
if(strcmp(sdp->d_name, ".") == 0 || strcmp(sdp->d_name, "..") == 0) {
continue;
}
//目录项本身不可访问,要拼接出访问路径
sprintf(path, "%s/%s", dir, sdp->d_name);
//判断文件类型,目录就递归进入,文件显示名字和大小
isFile(path);
}
closedir(dp);
return ;
}
void isFile(char *name) {
int ret = 0;
struct stat sb;
//获取文件属性,判断文件类型
ret = stat(name, &sb);
if(ret == -1) {
perror("stat error:");
return ;
}
//目录文件
if(S_ISDIR(sb.st_mode)) {
read_dir(name);
}
//普通文件
printf("%s\t%ld\n", name, sb.st_size);
return ;
}
int main(int argc, char *argv[]) {
//判断命令行参数
if(argc == 1) {
isFile(".");
} else {
isFile(argv[1]);
}
return 0;
}