Linux系统编程 Part4 (文件IO、文件存储)

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函数常见错误:

  • 打开文件不存在(即上述举例情况)
  • 以写方式打开只读文件
  • 以只写方式打开目录

笔记:

错误处理函数

  1. 直接输出errno:printf("xxx error: %d\n", errno);
  2. strerror函数:char *strerror(int errnum),示例:printf("xxx error: %s\n", strerror(errno));
  3. 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);

传入/传出参数

传入参数

  1. 指针作为函数参数
  2. 通常有const关键字修饰
  3. 指针指向有效区域, 在函数内部做读操作

示例:char *strcpy(char *dest, const char *src); 的第二个参数const char *src

传出参数

  1. 指针作为函数参数
  2. 在函数调用之前,指针指向的空间可以无意义,但必须有效
  3. 在函数内部,做写操作
  4. 函数调用结束后,充当函数返回值

示例:char *strcpy(char *dest, const char *src); 的第一个参数char *dest

传入传出参数

  1. 指针作为函数参数
  2. 在函数调用之前,指针指向的空间有实际意义
  3. 在函数内部,先做读操作,后做写操作
  4. 函数调用结束后,充当函数返回值

示例: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

思路:

  1. 判断命令行参数,获取用户要查询的目录名
    • 目录名:argv[1]
    • 如果用户没有传入目录,当作./处理,判断条件:argc == 1
  2. 判断用户指定的是否是目录:stat S_ISDIR(); –> 封装函数 isFile() { }
  3. 读目录伪代码:
 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;
}


不准投币喔 👆

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇