当前位置: 首页 > article >正文

Linux---文件io

1.系统调用

由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface,API)。是应用程序同系统之间数据交互的桥梁。

C标准函数和系统函数调用关系。一个helloworld如何打印到屏幕。

man手册中一共有九卷,其中一卷就有讲到系统调用,内核就当前操作系统的核心程序,系统的本质都是个程序。内核和硬件打交道,提供的函数只能给上层应用所使用。

提供的系统调用函数实际上在linux内核当中是没有的,只不过却有与之对应的一样功能的函数,比如open在内核当中的源码对应的是sys_open,虽然名字不同,但是几乎是一模一样的

sys_open >>浅封装>> open,操作系统避免与用户进行交互,但又不想让用户真正窥探到内核,因此使用了浅封装给内核中的sys_open包了个保护壳变成open函数让用户可以去调用系统而又不会导致让用户窥探到本质

2.open/close

函数原型:

要导入头文件 #include<unistd.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int close(int fd);
int creat(const char *pathname, mode_t mode);

 # 可以通过 man open 2 指令去查看配置的文档
 # pathname:文件路径
 # flags:打开方式
 # mode: 权限,mode_t是一个八进制的整型
 # 返回的是一个文件描述符,如果执行错误,会返回-1
 # 同时返回一个errno(操作系统的全局变量),需要导入 errno.h
 # 返回的errno的数字想要知道是什么含义可以通过 man strerror 去查看

常用参数 -- flags

要导入头文件:#include<fcntl.h>

O_RDONLY

O_WRONLY
O_RDWR读写
O_APPEND追加
O_CREAT创建
O_EXCL存放
O_TRUNC截断
O_NONBLOCK非阻塞

创建文件时,指定文件访问权限。权限同时受umask影响。结论为:

文件权限 = mode & ~umask

使用头文件:<fcntl.h>

O_CREAT -- 如果没有该文件就进行创建

创建文件时,其权限与umask挂钩,比如umask = 0002,取反后就是775,然后与mode进行按位与(二进制),最后才得出创建文件的真正权限

 O_TRUNC --- int ftruncate(int fd, off_t length); -- 把文件截断成0

open常见错误

1. 打开文件不存在

2. 以写方式打开只读文件(打开文件没有对应权限)

3. 以只写方式打开目录

perror、strerror、errno

运用

[....]# cd /linux_01
[....]# mkdir -a ./file_IO_test/test
[....]# cd ./file_IO_test/test
[....]# touch dict.txt
[....]# touch ./makefile
[....]# mkdir ./dirct
[....]# vim open.c
    #include<unistd.h>
    #include<stdlib.h>
    #include<fcntl.h>
    #include<string.h>
    int main(int argc,char *agrv[])
    {
         int fd = 0;
         fd = open("./dict.txt",O_RDONLY);
         printf("fd = %d\n",fd);

         int fd1 = 0;
         fd1 = open("./dict.cp",O_RDONLY | O_CREAT,0644);
         printf("fd1 = %d\n",fd);

         //1.打开文件不存在
         int fd2 = 0;
         fd2 = open("./dict.cp1",O_RDONLY);         
         printf("fd2 = %d, error = %d:%s\n",fd2,error,strerror(error)));

         //2.以写方式打开只读文件(打开文件没有对应权限)
         int fd3 = 0; 
         open("./dict.cp2",O_RDONLY | O_CREAT,0411); 
         //创建一个只读文件
         fd3 = open("./dict.cp2",O_WRONLY);
         printf("fd3 = %d, error = %d:%s\n",fd3,error,strerror(error)));
         //fd = -1,errno=13:Permissiondenied

         //3.以只写方式打开目录
         int fd4 = 0;
         fd4 = open("./dict.cp2",O_WRONLY);
         printf("fd4 = %d, error = %d:%s\n",fd4,error,strerror(error)));
         // fd = -1, errno=21:Is a directory                                                   

         close(fd);
         ......
         return 0;

     }

3.read/write函数

ssize_t read(int fd, void *buf, size_t count); 
    从指定位置fd读,然后将读取的东西存入缓冲区buf(待写出数据的缓冲区),count是数据的大小
    成功的话会返回读取到的字节数(读取到文件尾部会返回0),失败的话会返回-1,同时设置errno
ssize_t write(int fd, const void *buf, size_t count);

练习:编写程序实现简单的cp功能。
[....]# vim cptest.c
    #include<Stdio.h>
    #include<stdlib.h>  //perror所用到的头文件
    ....
    #include<fcntl.h>
    int main(int agrc,int **argv){
        char buf[1024];
        int n = 0;

        int fd1 = open(argv[1],O_RDONLY); //read
        if(fd1 == -1){
            perror("open argv1 error\n");
            // 如果读取出错就打印出自定义提示 
            exit(1);       
        }
        int fd2 = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
        # fd2是创建打开一个文件进行进行写入操作,如果该文件中有我内容则清0
        # 如果没有该文件就创建该文件,目的是将fd1的内容写入到fd2
        if(fd2 == -1){
            perror("open argv2 error\n");
            // 如果读取出错就打印出自定义提示 
            exit(1);       
        }

        //写入操作:
        while((n = read(fd1,buf,1024))!=0){
            if(n < 0){
                perror("read error");
                break;           
            }
            write(fd2,buf,n);
            // 第三个参数为n是为了防止缓冲区资源的浪费        
        }

        close(fd1);
        close(fd2);

        return 0;
    }
gcc cptest.c  -o cptest
./cptest.c open.c open2.c
# open.c是要拷贝过去的内容,open2.c是拷贝目的地

比较:如果一个只读一个字节实现文件拷贝,使用read、write效率高,还是使用对应的标库函数(fgetc、fputc)效率高呢?

理想上是write比fputc效率更高:

不管是fputc还是write,最终目的都是要从用户空间进入到系统内核,然后系统内核去驱动磁盘进行写入写出工作,但是只有系统函数才能才去调用系统内核让驱动工作,因此write效率更高,因为write本身就是系统调用提供的函数。而fputc并不是系统调用提供的函数,它底层是先去调用write,然后从用户空间进入到内核空间,借助驱动去驱动磁盘工作

但真是这样吗?

但实际上却是fputc比wirte效率更快:

strace ./可执行文件名称

可以看函数的调用过程

用read和write:read一次读一个,write一次写一个,一次只读取一个字节

用fgetc和fputc:并不是一个字节一个字节的操作,而是4096个字节的进行一次操作

探究:

fputc有自己的缓冲区,当缓冲区的东西满了后,才会去调用write进入到内核,将数据放入到系统级缓冲区,调用驱动进入到磁盘

而write和read没有自己的缓冲区,又因为人为的将buf缓冲区设置为1,因此只能不断地来回反复操作只读取或写入一个字节

----因此可以将buf缓冲区的大小重新设置好,就能实现wirte和read效率更高

read、write函数常常被称为Unbuffered I/O。指的是无用户及缓冲区。但不保证不使用内核缓冲区。-----预读入和缓输出

4.文件描述符

PCB进程控制块

使用命令查看其位置:
[....]# locate sched.h
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h

PCB进程控制块就像一个结构体
struct task_struct { 结构体 }

文件描述符

结构体PCB 的成员变量file_struct *file 指向文件描述符表。

从应用程序使用角度,该指针可理解记忆成一个字符指针数组,下标0/1/2/3/4...找到文件结构体。

本质是一个键值对0、1、2...都分别对应具体地址。但键值对使用的特性是自动映射,我们只操作键不直接使用值。

value中就是个指针指向了一个文件结构体,这个文件结构体中记录了进行操作文件的内容,比如属组、属主、路径等,但操作系统不想暴露这些value,因此只返回了key的值 -- 文件描述符

新打开文件返回文件描述符表中未使用的最小文件描述符。

  • STDIN_FILENO  0
  • STDOUT_FILENO  1
  • STDERR_FILENO 2

最大打开文件数:

一个进程默认打开文件的个数1024。

命令:ulimit -a 查看open files 对应值。默认为1024,可以使用ulimit -n 4096 修改;当然也可以通过修改系统配置文件永久修改该值,但是不建议这样操作。

cat /proc/sys/fs/file-max 可以查看该电脑最大可以打开的文件个数。受内存大小影响

FILE结构体

主要包含文件描述符、文件读写位置、IO缓冲区三部分内容。

struct file {
    ...
        文件的偏移量;
        文件的访问权限;
        文件的打开标志;
        文件内核缓冲区的首地址;
        struct operations * f_op;
    ...
    };

查看方法:

(1) /usr/src/linux-headers-3.16.0-30/include/linux/fs.h

(2) lxr:百度 lxr → lxr.oss.org.cn → 选择内核版本(如3.10) → 点击File Search进行搜索

        → 关键字:“include/linux/fs.h” → Ctrl+F 查找 “struct file {”

        → 得到文件内核中结构体定义

        → “struct file_operations”文件内容操作函数指针

        → “struct inode_operations”文件属性操作函数指针

5.阻塞和非阻塞

读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:

正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。

就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。

  • 阻塞读终端:【block_readtty.c】
  • 非阻塞读终端【nonblock_readtty.c】
  • 非阻塞读终端和等待超时【nonblock_timeout.c】

注意,阻塞与非阻塞是对于文件而言的。而不是read、write等的属性。read终端,默认阻塞读。

总结read 函数返回值:   

  1. 返回非零值:  实际read到的字节数
  2. 返回-1:
    1. errno != EAGAIN (或!= EWOULDBLOCK)  read出错
    2. errno == EAGAIN (或== EWOULDBLOCK)  设置了非阻塞读,并且没有数据到达。read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据
  3. 返回0:读到文件末尾

例子:

/dev/tty -- 终端文件,STDIN_FILENO和STDOUT_FILENO都是终端文件中的内容,实际上终端文件是已经跟着打开的了

标准输入和输出,当运行该文件时,如果不按键盘输入东西和点击回车,这时候终端就在等待,称为阻塞

阻塞是设备文件、网络文件的属性,不要误以为阻塞是read和write的特性,并且读常规文件无阻塞属性

那么能不能用非阻塞的方式去操作终端文件?

用open中的O_NONBLOCK非阻塞的方式打开tty终端文件,这时候只会反复的读,读到了就输出到屏幕,读不到就继续读,终端并不会进入等待状态(阻塞)

6.fcntl函数

【fcntl.c】 想改变文件的访问控制属性,比如从阻塞状态变成非阻塞状态,那就得关闭文件重新打开文件进行操作,而使用 fcntl 函数可以直接改变

改变一个【已经打开】的文件的访问控制属性,重点掌握这两个参数的使用,F_GETEL F_SETFL

F_GETFL 读取到标准输入终端文件的状态后,通过 |= 添加上非阻塞属性,然后用F_SETFL 将其重新设置,使标准输入变为非阻塞

7.lseek函数

理解

官方定义:重新定位读或写的文件偏移量。

Linux中可使用系统函数lseek来修改文件偏移量(读写位置)

  • 每当打开一个文件,都会有一个叫做“当前文件偏移量”的东西,如果难理解也可以将他理解为指针。 除非打开文件时指定O_APPEND选项,否则文件偏移量默认设置为0。当我们发生了一次读或者写操作时,都会使这个当前文件偏移量发生变化,读/写多少字节,当前偏移量就会向后移动多少。
  • 因此当我们对一个新文件进行完写操作后,进行读操作,会发现什么都读不到,是因为偏移量经过写操作后移到了文件尾部,此时进行读操作肯定什么都读不到了,也就是读和写操作用的是同一个偏移量(文件指针)

lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。

回忆fseek的作用及常用参数:
SEEK_SET、SEEK_CUR、SEEK_END
int fseek(FILE *stream, long offset, int whence); 
成功返回0;失败返回-1
特别的:超出文件末尾位置返回0;往回超出文件头位置,返回-1

include<lseek.c>
off_t lseek(int fd, off_t offset, int whence); 
    参数:fd //文件描述符,可以通过open函数得到,通过这个fd可以操作某个文件
    参数: offset //文件偏移量,是一个整形数,与whence对应的位置继续往后偏移
    参数:whence //偏移类型,下列三个值中选一个。

            SEEK_SET:该文件的偏移量设为离文件开头offset个字节.
            SEEK_CUR:该文件的偏移量设为其当前值加offset(PS :offest可正负).
            SEEK_END:该文件的偏移量设为文件长度加offset
特别的:lseek允许超过文件结尾设置偏移量,文件会因此被拓展。

注意文件“读”和“写”使用同一偏移位置。                                
lseek函数返回的偏移量(off_t)总是相对于文件头而言的。                                

作用

  1. 移动文件指针到文件头:lseek(fd, 0, SEEK_SET)
  2. 获取当前文件指针的位置:lseek(fd, 0, SEEK_SUR)
  3. 获取文件长度:lseek(fd, 0, SEEK_END)
  4. 拓展文件的长度,当前文件10b、110b,增加了100个字节:lseed(fd, 100, SEEK_END),需要注意拓展完需要再写一次数据,否侧拓展无效

还有另一种方式也可以来拓展文件的大小

[....]# main 2truncate  # 可以查找到另一个函数truncate
>>>int truncate(const char* path, off_t length)

例子

1.移动文件指针到文件头和获取当前文件指针的位置

//导入所有需要的头文件
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>

//我们的目的是:移动文件指针到文件头:
int main()
{
    //获取文件的文件描述符
    int fd = open("text.txt", O_RDWR);
    if (fd == -1)
    {
        perror("open");
        return -1;
    }

    //输出当前文件的偏移量
    long long int loc = lseek(fd, 0, SEEK_CUR);
    printf("%lld\n", loc);   //0

    //使用read函数读3个字节的数据
    char buf[3] = {0};
    int rnum = read(fd, buf, sizeof(buf));
    printf("%d\n", rnum);   //3

    //再次查看文件的偏移量(获取当前文件指针的位置)
    long long int loc1 = lseek(fd, 0, SEEK_CUR);
    printf("%lld\n", loc1);   //3

    //移动文件指针到文件头
    long long int loc2 = lseek(fd, 0, SEEK_SET);
    printf("%lld\n", loc2);   //0

    return 0;
}

2.获取文件长度

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
int main()
{
    //获取文件的文件描述符
    int fd = open("hello.txt", O_RDWR);


    //获取文件长度
    long long int loc1 = lseek(fd, 0, SEEK_END);
    printf("%lld\n", loc1);

    return 0;
}

3.拓展文件的长度(注:拓展完成后需要再写一次数据,否则拓展无效)

int main()
{
    //获取文件的文件描述符
    int fd = open("hello.txt", O_RDWR);


    //获取文件长度
    long long int loc1 = lseek(fd, 0, SEEK_END);
    printf("%lld\n", loc1);

    //拓展文件的长度,需要引起IO操作后文件的大小才会改变
    //可以通过 ll 指令查看文件的详细属性
    long long int loc2 = lseek(fd, 100, SEEK_END);
    write(fd," ",1);//写入一个空数据
    //该空数据前的100个字节是文件空洞,这是系统自动帮我们填补的:^@
    printf("%lld\n", loc2);


    return 0;
}

od -tcx filename  查看文件的16进制表示形式
od -tcd filename  查看文件的10进制表示形式

8.传入传出参数

传入参数:

1.指针作为函数参数

2.通常有const关键字修饰

3.指针指向有效区域, 在函数内部做读操作

char *strcpy(cnost char *src, char *dst);

传出参数:

1.指针作为函数参数

2.在函数调用之前,指针指向的空间可以无意义,但必须有效(如不能指向未初始化的空间)

3.在函数内部,做写操作

4.函数调用结束后,充当函数返回值

传入传出参数:

1.指针作为函数参数

2.在函数调用之前,指针指向的空间有实际意义

3.在函数内部,先做读操作,后做写操作

4.函数调用结束后,充当函数返回值

char strtok(char str, const char delim, char ** saveptr) 其中第三个参数就是传入传出参数


http://www.kler.cn/a/325075.html

相关文章:

  • 5.4.2-1 编写Java程序在HDFS上创建文件
  • Qt 的 QThread:多线程编程的基础
  • 解决docker mysql命令行无法输入中文
  • 【AI图像生成网站Golang】项目架构
  • 网络协议之UDP
  • ScubaGear:用于评估 Microsoft 365 配置是否存在安全漏洞的开源工具
  • delphi制作漂亮的农历窗体(IntraWeb+Layui的完美结合)
  • 【设计模式-中介者模式】
  • EHS管理系统设备安全设施安全监控模块
  • 用manim实现有想法的Pi
  • 能力成熟度模型集成(CMMI)
  • c# 结构体反射赋值问题 结构体 反射赋值
  • 百度智能体创建:情感领域的创新力量
  • 大模型训练:K8s 环境中数千节点存储最佳实践
  • 车辆零部件检测和分割数据集-车体数据集-yolo格式-yolov5-yolov10可用
  • docker-图形化工具-portainer的使用
  • Vue $router.push打开新窗口
  • 【Linux网络】详解TCP协议(2)
  • 网站建设中常见的网站后台开发语言有哪几种,各自优缺点都是什么?
  • python和pyqt-tools安装位置
  • 【从零开始实现stm32无刷电机FOC】【实践】【7.1/7 硬件设计】
  • 【Golang】关于Go语言字符串转换strconv
  • 《牧神记》PV初体验,玄机科技再塑经典国漫
  • 学习C++的第七天!
  • 新建flask项目,配置入口文件,启动项目
  • OceanBase 一级表分区记录