『 Linux 』基础IO/文件IO (万字)

文章目录

    • 🦄 什么是IO
    • 🦄 文件IO(库级别)
      • 👾 文件的打开与关闭
      • 👾 当前路径
      • 👾 文件的读写
    • 🦄 标准输入输出流
    • 🦄 文件IO(系统级别)
      • 👾 文件的打开
      • 👾 文件的关闭
      • 👾 文件的读写
    • 🦄 文件描述符(file descriptor)
      • 👾 FILE*指针与文件描述符fd的关系


🦄 什么是IO

请添加图片描述

在计算机当中,I/O即为输入和输出的英文缩写版,input/output;

而在计算机当中,任何与输入输出有关的操作都可以被称作为IO,例如简单的对于设备上的将信息以打印的方式在显示屏(终端)中进入显示或者是在文件的读写操作都可以被看成是一种IO行为;

一般在计算机系统当中,IO操作可以分为两大类:

  • 基于硬件层面的IO
  • 基于软件层面的IO

而若是再进行细分的话可以分为:

  • 网络IO
  • 设备IO
  • 内存IO
  • 标准输入/输出
  • 数据库IO
  • 进程间通信
本篇博客以CentOS7.6为例

🦄 文件IO(库级别)

请添加图片描述

在上文中列举常见的IO当中提到了 标准输入/输出 ;

而在 C语言 当中则存在一个 标准输入输出库<stdio.h>;

在这个头文件当中包含了许多关于IO操作的函数,这包括scanf(),printf()以及一些文件IO操作的接口;

对应的 文件IO操作 的函数分别有fopen(),fclose(),fwrite(),fread(),fscanf(),fprintf()等等;

当然这些操作对应着也会根据不同的层级平台拥有着不同的级别位;

对于这些在 C语言 的标准库当中出现的这些接口一般称为库级别的;

对应的除了库级别以外还有对应的系统级别的操作,但由于系统级别的操作可能会出现复杂或者冗余,故在其他高级语言当中会重新将这些接口进行封装优化,使得用户在调用时能够更加"随心所欲";


👾 文件的打开与关闭

请添加图片描述

而对于Linux操作系统而言 “一切皆文件” ;

当然,在C语言当中存在一个名为FILE的类型,这个类型是C语言当中表示一个文件流的一个抽象数据类型;

  • fopen()

           FILE *fopen(const char *path, const char *mode);
    

    该函数用于打开文件;

    当文件打开成功时将返回一个FILE*的指针;

    若是文件打开失败则返回空指针NULL;

    • const char *path

      该参数表示传入一个字符串作需要打开文件的文件名;

      可在传入的文件名当中添加相对路径或者是绝对路径,若是未传路径而是单纯的只传文件名那么该文件操作将设置文件打开路径为 当前路径;

    • const char *mode

      该参数表示传入一个字符串参数表示打开文件时的方式;

      一般的打开方式有a,w,a+,w+等等;

      传参打开方式详细
      “r”以只读方式打开文件必须存在,否则打开失败;
      “r+”以读/写方式打开文件必须存在,否则打开失败;
      该模式允许对文件进行读和写操作;
      “w”以只写方式打开如果文件存在,则其内容会被截断为0,即内容会被清空;
      如果文件不存在,则会创建一个新文件;
      “w+”以读/写方式打开如果文件存在,则其内容会被截断为0,即内容会被清空;
      如果文件不存在,则会创建一个新文件;
      该模式允许对文件进行读和写操作;
      “a”以追加方式打开如果文件存在,写操作会从文件末尾开始添加内容,文件原有的内容不会被截断;
      如果文件不存在,则会创建一个新文件;
      “a+”以读/追加方式打开如果文件存在,写操作会从文件末尾开始添加内容,文件原有的内容不会被截断;
      如果文件不存在,则会创建一个新文件;
      这种模式允许对文件进行读和写操作,但所有写操作都会追加到文件末尾;

      Linux当中存在一个符号为重定向;

      这个符号一般为>或是>>;

      重定向的底层实际上就是一个将文件打开并进行写入的一个过程;

      只不过是打开的方式不同;

      • >

        该重定向与fopen("w")方式相同;

        即以只写的方式打开,文件若是存在则清空文件,文件若是不存在则创建文件;

      • >>

        该重定向与fopen("a")方式相同;

        即以追加的方式将文件打开,文件若是存在则在文件的最末尾处进行写入;

        文件若是不存在则创建文件;

  • fclose()

           int fclose(FILE* fp);
    

    该函数用于关闭一个文件;

    FILE* fp表示传入一个FILE*类型的文件流指针;

    该函数若是关闭成功则返回0,若是关闭不成功则返回EOF(文件结束标志)表示文件关闭失败;


👾 当前路径

请添加图片描述

在上文当中提到fopen()函数在打开时的文件名传参;

在传const char *path时可以在文件名中加入对应的绝对路径或者相对路径;

而若是未在文件名中加入路径,则文件的打开路径默认为 当前路径 ;

  • 那么这里的 当前路径 指的是什么当前路径, 当前路径 又是谁的路径?

实际上当前路径所指的路径是进程的路径;

当一个程序被运行时其将会在内存当中化为进程,而进程实际上也需要有对应的运行路径;

  • 存在一段代码

    int main(){FILE* fp = fopen("log.txt","w");if(!fp){perror("fopen");return 1;}pid_t id = getpid();printf("%d\n",id);sleep(1000);fclose(fp);return 0;
    }
    

    在这段代码当中利用fopen()函数以w的方式打开一个名为log.txt的文件;

    使用getpid()函数打印该进程的PID;

    打印结束后调用sleep()函数使得进程进入睡眠;

  • 运行这段代码后得出的结果为

    28252
    

Linux当中,可以在/proc/(PID)路径下找到对应PID进程的信息;

其中在进程的信息中存在一条cwd的信息存放着进程的工作路径;

$ ls -l /proc/28252 | grep cwd
lrwxrwxrwx 1 _XXX _XXX 0 May  2 21:39 cwd -> /home/_XXX/Begin/my_-linux/Pro24/IO/read_or_write

从该段指令以及结果中可以看到,在使用指令查找对应cwd时返回了一个路径/home/_XXX/Begin/my_-linux/Pro24/IO/read_or_write;

而实际上这个路径即为进程当前的工作路径,也就是上文中所提到的 当前路径 ;

$ ls /home/_XXX/Begin/my_-linux/Pro24/IO/read_or_write
log.txt  makefile  mytest  test.c

从结果中可以看出,在进程的工作路径当中生成了一个名为log.txt的文件;

当然还可以使用另一种方式进行验证;

  • <unistd.h>头文件当中存在一个函数:

           int chdir(const char *path)
    

    这个函数能够在当前进程中修改进程的工作路径;

    当工作路径成功被修改时,该函数将返回0,若是工作路径修改失败,对应的函数将会返回-1;

既然创建文件时的当前路径指的是进程的当前路径,若是这样的话使用chdir()转移进程对应的工作路径后则能在对应的路径当中找到新建的文件;

  • 存在一段代码:

    int main(){chdir("./../../../..");FILE* fp = fopen("log.txt","w");if(!fp){perror("fopen");return 1;}pid_t id = getpid();printf("%d\n",id);sleep(1000);fclose(fp);return 0;
    }
    

    这段代码与上段代码并无太大差异,在这段代码当中仅仅是调用了chdir()函数从而改变进程的工作路径从而验证 “当前路径” 指的是进程的当前路径;

    运行该段代码对应的结果为:

    $ ./mytest 
    31545
    

    利用上文当中出现的查找进程工作路径的方式一样进行验证;

    $ ll /proc/31545 | grep cwd
    lrwxrwxrwx 1 _XXX _XXX 0 May  3 10:23 cwd -> /home/_XXX/Begin
    

    验证过后发现该进程的当前工作路径由/home/_XXX/Begin/my_-linux/Pro24/IO/read_or_write变为了/home/_XXX/Begin;

    查找对应的Begin目录当中是否存在新建的文件;

    $ ls /home/_XXX/Begin/
    log.txt  my_-linux
    

👾 文件的读写

请添加图片描述

C语言 当中对于文件的读写通常是采用标准库当中提供的一些接口函数;

这些接口函数通常与fopen(),fclose()等函数一起使用;

  • fwrite()

           size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
    

    该函数为写入内容至文件,一般与fopen("w")配合一起使用;

    • const void *ptr

      该参数表示根据用户需求传入一个const void*的指针;

      这意味着用户可以根据需求传入需要的数据(不一定非要是字符串,可根据需要传入任意类型的指针);

    • size_t size

      该参数表示用户在传入数据后应当传入所写入数据当中每个数据块的大小(字节);

      • 那么假设传入一组数据位为字符串,在传该参数的使用使用了strlen()计算字符串的大小,strlen()过后的数据是否需要+1?为什么?

        对于以往的关于字符串的函数的传参而言,当在进行传参的时候需要进行+1,原因是在 C语言 当中字符串是以\0作为结束标志的;

        故当若是需要进行拷贝或者其他操作的时候通常需要将\0的数量划到 字符串 的区域内;

        那么在这个参数的传参当中是否需要将\0与字符串混为一谈?

        答案是不用;

        对于 C语言 来说,将\0划为字符串的结束标志是因为 C语言 在内存当中不能很好的去标明一个字符串的结束,故需要一个非显字符作为字符串的结束标志;

        而在 C语言 当中所定的规则实则在OS当中并不适用,OS本身可以识别数据类型,故当调用该函数传该参数时并不需要考虑\0的个数(占位);

    • size_t nmemb

      该参数表示用户需要传入缩写数据的数据块个数;

    • FILE *stream

      该参数表示用户在给文件写入数据时必须给定要在哪个文件当中进行写入,故对应的应该将文件的文件流FILE*指针进行传入才能进行后续操作;

    • Return Value

      若是写入成功,函数将返回写入元素的个数;

      对应的若是返回的数据与nmemb参数不同或是为0则表示可能表示发生了错误或到达了文件末尾;

  • fread()

           size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    

    该函数为从一个文件当中读取数据,通常配合fopen("r")一起使用;

    该函数的参数与fwrite()函数的参数大致相同;

    唯一不同的是对于ptr而言,在fwrite()函数当中将传入一个const void*的指针从而能够将需要写入的数据传入至函数内部;

    而在fread()函数当中应确保有一块足够大以至于能够容纳从文件当中读取到的数据的一块空间,故需要传入对应的地址;

  • fprintf()

           int fprintf(FILE *stream, const char *format, ...);
    

    该函数与fwrite()函数的作用相同,即为传入一个FILE*文件流指针,并将对应的信息传入至该文件流当中;

    该函数同样一般需要配合fopen()函数进行使用,即在使用该函数的时候确保文件要被可写入的方式打开;

    该函数与printf()函数中唯一不同的点就是需要传入一个文件流指针从而保证对应的数据信息能够通过文件流写入到对应的文件当中;

    其余参数与printf()函数无异;

  • fscanf()

    int fscanf(FILE *stream, const char *format, ...);
    

    该函数与fprintf()函数的情况相同,在此不再进行过多赘述;

具体其他与文件相关的接口函数在此不再进行赘述;

很有趣的一件事是对于上述文件的接口函数的函数名可以进行一个思考;

对于printf()scanf()函数而言,其可以看成是直接打印数据至终端或者是从输入设备(键盘)中读取数据;

而对于fprintf()fscanf()函数而言只是将对应的终端/输入设备转化为了文件流;


🦄 标准输入输出流

请添加图片描述

在上文中提到了 C语言 当中的与文件相关的接口;

  • 那么联系上文的接口思考"Linux"下一切皆文件是否与其有对应关系?

    在C语言的学习当中有着这么一句话:

    • 在C语言当中,默认会打开三个标准输入输出流(标准输入,标准输出,标准错误),分别为stdin,stdout,stderr;

    Linux当中使用man来查找对应的信息man stdin;

    对应的信息可以看到:

    NAMEstdin, stdout, stderr - standard I/O streamsSYNOPSIS#include <stdio.h>extern FILE *stdin;extern FILE *stdout;extern FILE *stderr;
    

    在手册中可以发现实际上三个标准输入输出流的类型都是为FILE*的类型;

    FILE*类型又是文件流的类型,以 Linux操作系统当中一切皆文件 来理解的话是否可以将对应的硬件设备也视为文件;

    且是否可以使用fprintf()或者fscanf()来调用对应的设备;

根据上面的假设可以进行验证;

  • 存在一段代码

    #include <stdio.h>int main() {char str[100] = {0};str[99] = '\0';printf("从stdin中读取数据:\n");fscanf(stdin, "%99s", str);printf("在stdout中写入数据:\n");fprintf(stdout, "%s\n", str);printf("在stderr中写入数据:\n");fprintf(stderr, "%s\n", str);return 0;
    }
    

    这段代码定义了一个str的数组作为接收数据的空间;

    调用fscanf()函数并将stdin即标准输入流作为文件流参数传入函数当中,以str作为数据的中转站,即将从stdin文件流中读入的数据保存至str当中;

    调用了fprintf()函数,并分别将标准输出stdout与标准错误stderr作为文件流的参数传入至函数当中,将str数据写入至stdoutstderr文件流当中;

运行程序后发现在打印从stdin中读取数据:后进程将会阻塞;

原因是因为fscanf()需要从stdin文件流中读取数据,然而stdin文件流中并没有任何数据;

意思是需要从键盘当中读取数据;

当从键盘当中获取数据后进行回车时对应的信息将会被打印两次;

原因是以当前计算机而言,stdin默认指的是键盘,而stderrstdout默认指的是显示器;

同时可以进行验证 Linux下一切皆文件” ;

从演示中可以看出,对于操作系统而言,键盘与显示器等硬件设备也是被以文件的形式所看待;

实际上并不是只有 C语言 当中存在上述所提到的 “在C语言当中,默认会打开三个标准输入输出流”,任何语言在 默认会打开三个标准输入输出流 这句话当中都是适用的;

  • 打开这三个标准输入输出流实际上是操作系统的行为

  • 其每个流所对应的硬件如下:


🦄 文件IO(系统级别)

请添加图片描述

该图列出了对应的计算机体系的组成部分;

以该图为例,若是由下往上看的话为每一个底层都在直接/间接的服务以它为基础的上层;

而若是从上往下观察,实际上在访问的过程当中也不能越过其中的任何一个障碍去进行访问;

  • 以用户访问硬件为例:

    用户访问硬件必须以贯穿的形式进行访问而不能越过任何一个其他步骤;

在上文当中提到了库级别的文件IO接口函数(libc);

而正如刚刚所说的,服务是层级向上的;

而访问是贯穿向下的,libc也是如此,实际上libc是由 系统调用(System Call)进行封装的;

对于在上文中提到的接口函数(例如fopen(),fclose(),fwrite()等…)作为 C语言 的库函数(libc)都存在于 C标准库中的 <stdio.h>头文件当中;

而系统调用都存在于 系统库 的头文件当中,在Linux当中,常用的头文件有:

  • <sys/types.h>

    该头文件当中定义了一系列的数据类型,这些类型通常用于系统调用;

  • <sys/stat.h>

    提供了访问文件状态信息的功能,比如文件权限,最后修改时间等;

  • <sys/wait.h>

    提供了进程控制相关的功能,比如等待子进程结束;

  • <sys/mman.h>

    提供了内存管理功能,包括内存映射,保护,解除映射等;

  • <sys/socket.h>

    提供了创建套接字,监听套接字,接受连接等网络编程相关的功能;

  • <sys/ioctl.h>

    提供了对设备I/O操作的接口,比如终端,网络接口的控制;

  • <sys/time.h><sys/times.h>

    提供了时间相关的功能,包括设置时间,获取时间等;

  • <sys/resource.h>

    提供了资源控制的功能,比如设置和获取进程优先级,资源限制等;

  • <sys/uio.h>

    提供了向量I/O操作的功能,允许一次性从多个缓冲区读写;

对于 C标准库 而言,由于是标准库,所以具有可移植性,可在多个平台中以统一的规则去调用同样的接口函数;

系统库 不同, 系统库 顾名思义即为根据不同操作系统所设计出来的库,一般来说这种库是并不具有可移植性;

举个最简单的例子,在上面提到了许多 <sys/ ... .h> 的头文件,由于这些头文件是Linux当中的系统调用,在Windows本身是不适用的;


👾 文件的打开

请添加图片描述

在上文当中提到了库级别的文件IO操作函数;

库级别的文件打开使用的是fopen()函数,当文件打开成功后将会返回一个FILE*的文件留置针,打开失败后将会返回NULL空指针;

而在Linux当中打开文件使用的函数为open()函数;

fopen()不同,open()函数为一个系统调用级别的接口;

NAMEopen, creat - open and possibly create a file or deviceSYNOPSIS#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);

该段代码为在Linux当中使用man手册查看open()函数的结果;

从代码中看出若是需要调用该函数则需要包含三个头文件<sys/types.h>,<sys/stat.h><fcntl.h>;

同时open()函数共有两个版本,但两个版本的差异仅仅为一个参数的差异;

但实际上两个函数确实是根据不同的需求所使用的,对应的相差的那个参数mode_t mode即为权限的参数;

同时对于接口的返回值其将会返回一个int类型的值;

当文件打开失败后将会返回-1作为erro的判断;

当打开成功时将会返回一个new file descriptor 该参数即为 文件描述符 ;

文件描述符将在下文进行讲解,目前只需要了解其可以代表一个文件即可
  • int open(const char *pathname, int flags);

    该函数一般用于打开 已经被创建/已经存在 了的文件;

  • int open(const char *pathname, int flags, mode_t mode);

    该函数既可以用于打开 已经被创建/已经存在 的文件,也可以创建文件;

同时在该函数的man手册当中可以看到,实际上在该函数当中存在许多选项;

而这些选项实际上对应的是该函数的选项;

一般常用的选项如下:

  • O_RDONLY - 以只读的方式打开文件
  • O_WRONLY - 以只写的方式打开文件
  • O_RDWR - 以读写的方式打开文件
  • O_APPEND - 以追加的方式打开文件
  • O_CREAT - 若是文件不存在则创建文件
  • O_TRUNC - 若是文件存在则清空文件

而在open()函数的传参的方式并不与以往的函数传参相同;

  • 以只读的方式打开文件

    open("filename",O_RDONLY);
    
  • 以只读的方式打开文件,若是文件不存在则新建文件

    open("filename",O_RDONLY|O_CREAT);
    

从这两行代码当中可以观察到实际上该函数的传参方式为 比特位级别的标志传参方式 ,从而能够使得在一个参数位当中传递多种信息从而使得传参能够存在一定的灵活性;

那么比特位级别的标志传参方式是如何进行的?

已知二进制是以 2n 进行分布;

1byte == 8bit

由左至右分别为 27 , 26 , 2520 ;

以该图为例;

而从第一行可以看出,实际上一个数至代表一个位置,其具有唯一性;

而以位的方式进行传参正是依赖于这种这种传参的唯一性;

  • 存在一段代码
#define ONE (1 << 0)    // 1
#define TWO (1 << 1)    // 2
#define THREE (1 << 2)  // 4
#define FOUR (1 << 3)   // 8
//其中上述定义了四个掩码都是唯一的掩码
//当在进行与&的操作时将判断该位置上是否为1 从而能够进行精准的传参
void show(int flag) {if (flag & ONE) {printf("ONE\n");}if (flag & TWO) {printf("TWO\n");}if (flag & THREE) {printf("THREE\n");}if (flag & FOUR) {printf("FOUR\n");}
}int main() {show(ONE);printf("-------------------------\n");show(ONE | TWO);printf("-------------------------\n");show(TWO | THREE);printf("-------------------------\n");show(ONE | FOUR);printf("-------------------------\n");return 0;
}

在这段代码当中,使用了 #define 定义了几个宏(以位运算的方式);

这几个宏分别代表了4个掩码;

即为上图中出现的 23 , 22 , 21 , 20 ;

  • 运行结果
$ ./mytest 
ONE
-------------------------
ONE
TWO
-------------------------
TWO
THREE
-------------------------
ONE
FOUR
-------------------------

从运行的结果可以看到,所打印的与代码中的测试无异;

同时对于使用 比特位级别的标志传参方式 并不单单是因为其具有唯一性灵活性和高效率;

同时其还具有兼容性;

位操作是在所有支持 C语言 的平台都可使用的操作,这代表这个接口函数可以在大部分的操作系统当中保持一致;

根据上文的介绍可以大致明白open()函数的调用方式;

假设存在一个文件夹,其内容大致如下:

$ ls -l
total 20
-rw-rw-r-- 1 _XXX _XXX    81 May  3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10984 May  3 11:55 mytest
-rw-rw-r-- 1 _XXX _XXX   469 May  6 16:14 test.c

在这个目录当中存在三个文件,分别为makefile,mytest,test.c;

test.c文件的main()内容如下(头文件不过多赘述):

int main()
{int ret = open("log.txt", O_WRONLY);if (ret < 0) {printf("open fail\n");printf(-1);}return 0;
}

在这段代码当中调用open()函数并传参O_WRONLY,以 只写 的方式打开log.txt文件,若是打开文件失败将会打印open fail并退出进程;

运行该程序后其结果为:

$ ./mytest 
open file

从结果可以看出这里的文件打开失败;

其对应的文件也并未被创建:

$ ls -l
total 20
-rw-rw-r-- 1 _XXX _XXX    81 May  3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10808 May  6 16:26 mytest
-rw-rw-r-- 1 _XXX _XXX   573 May  6 16:26 test.c

原因是因为在上文当中我们比较了关于open()函数与fopen()函数的差异;

对于fopen()函数而言,其在文件不存在时将会新建一个对应的文件,而对于open()而言其并不会主动去新建文件,故在尝试使用open()函数打开一个不存在的文件时将会打开文件失败;

若是需要open()函数在打开文件时使其在文件不存在的情况下新建文件则需要添加新的参数,即为:

int ret = open("log.txt", O_WRONLY|O_CREAT);

添加对应的参数后对应的代码为:

int main()
{
int ret = open("log.txt", O_WRONLY|O_CREAT);if (ret < 0) {printf("open fail\n");printf(-1);}return 0;
}
$ ./mytest 
$ ll
total 20
--wsr-x--- 1 _XXX _XXX     0 May  6 16:37 log.txt
-rw-rw-r-- 1 _XXX _XXX    81 May  3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10808 May  6 16:37 mytest
-rw-rw-r-- 1 _XXX _XXX   581 May  6 16:37 test.c

修改代码后再次运行程序时发现并没有报错;

且对应的在目录当中出现了一个名为log.txt的文件;

  • 但可以发现一点问题

    已知在Linux当中权限一般以rwx的方式进行展示,但是在此处的log.txt文件当中其对应的权限变为了--wsr-x---;

    而实际上这是一个乱码,即在创建文件的时候并没有按照对应的权限进行创建;

文件的权限是与文件相伴而生的,同时在上文当中提到open()函数有两个版本;

  • 一个版本一般用于打开已存在的文件

    int open(const char *pathname, int flags);
    
  • 一个版本既可以打开已存在的文件也可以打开不存在的文件

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

而只有第二个版本的open()函数才能指定新建文件时文件的权限;

其中mode_t mode即为权限,一般采用八进制的方式进行传参;

int main()
{int ret = open("log.txt", O_WRONLY|O_CREAT,0777);if (ret < 0) {printf("open file\n");return(-1);}return 0;
}

修改代码,在代码当中添加文件新建时所伴随的权限为0777;

将原有的log.txt文件删除并重新运行程序;

$ ll
total 20
-rwxrwxr-x 1 _XXX _XXX     0 May  6 16:47 log.txt
-rw-rw-r-- 1 _XXX _XXX    81 May  3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10808 May  6 16:47 mytest
-rw-rw-r-- 1 _XXX _XXX   586 May  6 16:46 test.c

从最终结果来看,文件确实是被成功创建,同时伴随着权限;

我们指定以0777的方式创建文件,但最终其权限为0775,是因为在当前环境当中(我的环境)对应的umask0002;

$ umask
0002

而最终的文件权限为 原有权限&(~umask);

当然也可以在当前进程当中设置umask;

int main()
{umask(0);//设置当前进程的umaskint ret = open("log.txt", O_WRONLY|O_CREAT,0777);if (ret < 0) {printf("open file\n");return(-1);}return 0;
}

运行结果为:

$ ll
total 20
-rwxrwxrwx 1 _XXX _XXX     0 May  6 16:58 log.txt
-rw-rw-r-- 1 _XXX _XXX    81 May  3 11:47 makefile
-rwxrwxr-x 1 _XXX _XXX 10856 May  6 16:58 mytest
-rw-rw-r-- 1 _XXX _XXX   601 May  6 16:58 test.c

👾 文件的关闭

请添加图片描述

在C语言当中的文件关闭为fclose()函数,其中参数的传递为其对应的文件流FILE*的指针;

而在系统调用当中采用的函数为close()函数;

NAMEclose - close a file descriptorSYNOPSIS#include <unistd.h>int close(int fd);

在上文当中提到了open()函数的返回值将会返回一个int类型的 文件描述符 ;

而文件描述符实际上是代表一个文件的;

在使用 close() 函数关闭文件的时候只需要传入对应的文件描述符其就能将该文件关闭;

对于其返回值而言,当文件关闭成功时将返回0;

若文件关闭失败则返回-1并设置error;


👾 文件的读写

请添加图片描述

C语言 当中对于文件的读写的接口函数多种多样;

fread(),fwrite(),fprintf(),fscanf()…;

而在Linux当中,操作文件的系统调用接口函数主要为write()read();

  • read()

    NAMEread - read from a file descriptorSYNOPSIS#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
    

    对于系统调用接口的从文件中读取数据主要依靠read()函数;

    从上述信息可以看出read()在头文件<unistd.h>当中;

    而其对应的三个参数分别为:

    • int fd

      文件描述符(file descriptor),这个参数为int类型,代表要操作的文件,该参数一般是通过调用open()函数得到的;

    • void *buf

      这个参数为指向了一个缓冲区的指针,函数从文件中读取的数据将会被存放到这个缓冲区当中;

      由于这个参数的类型为void*的类型,这意味着这个指针可以是任何类型的指针;

    • size_t count

      这个参数指定了要读取数据的大小,一般以字节为单位;

      fread()函数相同,在计算大小时并不需要+1来区别对待字符串结束标志\0;

    其返回值的类型为ssize_t类型的数据(POSIX标准定义的一个数据类型,与size_t不同,这是一个有符号的数据类型);

    当文件读取成功时将会返回读取到的数据的大小;

    若是读取失败时则会返回-1并设置error;

  • write()

    NAMEwrite - write to a file descriptorSYNOPSIS#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
    

    read()函数相同,write()函数也是对于文件操作的系统调用接口函数中较为主要的一个函数;

    主要的功能为将数据写入文件当中;

    其对应的三个参数范别为:

    • int fd

      文件描述符,一般由调用open()函数获得;

    • const void* buf

      这个参数与read()函数的缓冲区相同;

      该参数为指向一个缓冲区的指针,而该缓冲区包含了要写入文件的数据,其中const void*表示其可以指向任何类型的数据类型;

      但是在进行写入操作(write)的时候缓冲区的内容不会被改变,故使用consst修饰;

    • size_t count

      该参数表示需要写入的数据大小(字节数);

    其返回值类型与read()函数相同,返回一个有符号的数据ssize_t;

    若是写入成功,将会返回写入数据的大小(字节);

    若是写入失败,将会返回-1并设置error;

这两个函数都是底层的系统调用,将会直接与操作系统内核交互,不经过任何缓冲;

两个函数都是POSIX标准的一部分;

在上文 文件IO(系统级别) > 文件的打开 部分中介绍了使用open()函数时要设置对应的写入方式;

文件的打开方式决定了文件在写入时的方式;

  • 存在一段代码

    int main()
    {umask(0);int fd = open("log.txt", O_WRONLY );char *str = "xxxxxxxxxxxxxxx\n";if (fd < 0) {printf("open file\n");return(-1);}write(fd, str, strlen(str));return 0;
    }
    

    在这段代码当中以 只读 的方式打开一个已经存在的log.txt文件并设置了一个名为str的缓冲区,其中缓冲区的内容为"xxxxxxxxxxxxxxx\n";

    当运行这段程序后使用cat打印出log.txt文件的内容后结果如下:

    $ cat log.txt 
    xxxxxxxxxxxxxxx
    

    从结果来看,这段程序已经成功的将对应的数据写入到了文件当中;

    但若是再多次运行该程序呢?

    $ cat log.txt 
    xxxxxxxxxxxxxxx
    

    即使多次运行该程序其最终的结果也相同;

    在原代码的基础上进行修改,将缓冲区的内容修改为"aaa\n"并再次运行程序;

    $ cat log.txt 
    aaa
    xxxxxxxxxxx
    

    当将缓冲区的内容进行替换之后发现"aaa\n"被写入至了文件当中;

    在调用open()函数时单纯以只读的方式打开文件时 文件的读写位置将在文件的开头;

    实际上系统接口调用级别的读写与库级别的读写相同,打开的方式决定了读写的位置;

    同时在上文介绍open()函数时提到了一个名为O_APPEND的选项;

      O_APPENDThe file is opened in append mode.  Before each write(2), the file offsetis positioned at the end of the file, as if with lseek(2).  O_APPEND  maylead  to  corrupted  files  on  NFS file systems if more than one processappends data to a file at once.  This is because  NFS  does  not  supportappending to a file, so the client kernel has to simulate it, which can'tbe done without a race condition.
    

    若是以该方式打开文件则文件将以追加的方式进行读写;

    同样的将该上述代码进行修改;

    int main()
    {umask(0);int fd = open("log.txt", O_APPEND );char *str = "aaaa\n";if (fd < 0) {printf("open file\n");return(-1);}write(fd, str, strlen(str));return 0;
    }
    

    打开log.txt文件手动清空后运行该程序;

    $ ./mytest 
    $ cat log.txt 
    $ 
    

    但从结果来看,似乎并没有数据被写入进log.txt文件当中;

    原因是因为在使用O_APPEND打开文件进行写入时并没有指定以读的方式打开文件,此时对应的文件的写入位置将会挪动到文件末尾;

    O_APPEND选项只会移动文件的读写位置,而其本身并不提供读写权限,若是需要提供读写权限则要追加O_WRONLY的选项;

    int fd = open("log.txt", O_APPEND );修改为 int fd = open("log.txt", O_APPEND|O_WRONLY );并再次运行程序;

    $ cat log.txt 
    $ ./mytest 
    $ cat log.txt 
    aaaa
    $ ./mytest 
    $ cat log.txt 
    aaaa
    aaaa
    $ ./mytest 
    $ cat log.txt 
    aaaa
    aaaa
    aaaa
    

    成功以追加的方式将数据写入文件当中;

在上文当中举例了使用O_APPEND选项打开文件并进行写入;

从这个选项当中可以看出该接口的组合使用方式与C语言当中的库级别的文件接口基本相似;

而事实也是如此,在Linux环境下,C语言的库级别的文件操作接口正是封装了系统调用接口;

fopen()为例,在使用fopen()打开文件时将存在几个选项,以常用的aw为例(关于写入方式);

  • fopen("filename","a")

    a的方式打开文件进行写入操作时,将是以追加的方式进行写入,若是文件不存在则新建文件;

    根据描述可以看出与上文中的O_APPEND选项相似,但是稍加组合就可以成为:

    open("filename", O_WRONLY|O_APPEND|O_CREAT , 0777);
    

    即以追加(文件读写位置位于文件的末尾)写入的方式打开文件,若是文件不存在则新建文件;

  • fopen("filename","w")

    w的选项打开文件也是如此;

    open("filename", O_WRONLY|O_TRUNC|O_CREAT , 0777);
    

    即为以读写的方式打开文件,若是文件不存在则新建文件,若是文件存在则清空文件,且文件的读写位置位于文件的开头;


🦄 文件描述符(file descriptor)

请添加图片描述

在上文当中提到了在调用open()函数时将返回一个int类型的返回值,而这个返回值即为 文件描述符 ;

  • 那么什么是文件描述符?

在上文当中提到,在计算机当中,由上层至下层的顺序依次为:

  • 用户 -> 用户操作接口 -> 系统调用(System cell) -> 操作系统 -> 驱动程序 -> 硬件

每一个层次都在以直接或者间接的形式为上层提供服务;

且以用户的视角来看其是无法绕过中间任何一层的,只能通过贯穿的形式逐级乡下进行访问;

用户无法直接访问操作系统;

但为了使得用户可以成功的与OS进行交互,那么其就必须为用户提供对应的系统调用处接口;

而在 操作系统 当中,无论是硬件还是文件都需要被操作系统进行管理,而管理的方式一般为 先描述,后组织;;

如上图所示,在Linux内核当中,每当打开一个文件都要对对应的文件进行管理,在进行描述的过程当中将为该打开的文件创建一个对应的结构体变量struct file,并在这些结构体变量当中存储文件的信息;

所存储的信息主要包含为:

  • 文件在磁盘的位置
  • 文件的基本属性,大小,读写位置和打开者
  • 文件内核缓冲区信息
  • 对应的struct file*next/prev指针(并不是所有内核都会将struct file结构体以双链表的形式进行管理)
  • 引用计数(判断引用计数是否为0从而判断是否需要将该struct file结构体对象进行回收)

在之前的有关进程的博客当中提到,当一个程序被运行时,OS也要对其以 先描述后组织 的方式进行管理;

实际上进程与文件之间的关系是属于一对多的,即一个进程可以打开多个文件;

如图所示,进程被运行后OS将为其创建一个task_struct进程控制块从而对该进程进行管理操作,而在这个进程控制块当中存在着一个指向struct file_struct结构体变量的指针,这个指针指向了一个file_struct的结构体变量,在这个结构体变量当中存在着一个名为struct file*fd_array[]的指针数组;

而这个struct file*fd_array[]指针数组即为对应的文件描述符表;

而对应的:

  • fd_array[]数组的下标即为文件描述符(file descriptor)

在这个指针数组中每个元素的类型即为struct file*对应着刚才所说的文件打开时为管理文件所创建的struct file数据结构;

可以进行猜测,当打开一个文件时将创建一个对应的struct file数据结构对应的进程也将在文件描述符表与该数据结构产生联系,调用open()函数返回其对应的下标;

每当需要对文件进行操作时则传入对应的文件描述符即可;

既然是fd_array[]数组的下标,且是int类型,这意味着该数据是可以被打印的;

  • 存在一段代码:

    int main() {int fd1 = open("log.txt", O_WRONLY | O_CREAT,0777);int fd2 = open("log.txt", O_WRONLY | O_CREAT,0777);int fd3 = open("log.txt", O_WRONLY | O_CREAT,0777);int fd4 = open("log.txt", O_WRONLY | O_CREAT,0777);printf("fd1 = %d\n", fd1);printf("fd2 = %d\n", fd2);printf("fd3 = %d\n", fd3);printf("fd4 = %d\n", fd4);return 0;
    }
    

    在这段代码当中以写的方式打开4个文件,并将对应的返回值(文件描述符)进行打印;

    $ ./mytest 
    fd1 = 3
    fd2 = 4
    fd3 = 5
    fd4 = 6
    

在上文中提到了一个概念 “在操作系统当中,默认会打开三个标准输入输出流” ;

而在该程序中的打印结果来看,对应返回的文件描述符是从3开始的;

这意味着在文件描述符表当中,下标为0,1,2已经被占用,那么着三个文件描述符是否可以对应 输入 , 输出 , 错误 ?

  • 验证

    在上文当中提到了,在Linux当中的系统调用级别的文件操作接口是以文件描述符来进行操作的;

    那么对应的0,1,2也是文件描述符,这意味着可以直接传入这三个文件描述符从而验证他们的关系;

    int main() {char buf[1024];buf[1024] = 0;ssize_t ret = read(0, buf, sizeof(buf));if (ret > 0) {buf[ret] = '\0';write(1, buf, strlen(buf));write(2, buf, strlen(buf));}return 0;
    }
    

    在这段代码当中,从文件描述符0当中获取数据并存储至缓冲区buf当中;

    再将数据写入至文件描述符1,2的文件当中;

    从结果来看,当从文件描述符0当中获取数据时进程将会进行阻塞并等待键盘的输入;

    当从键盘中读取数据后将数据写入文件描述符1,2时对应从键盘中获取的数据将会被打印在显示器当中;

    同时在在上文当中提到,对于操作系统级别的文件操作接口的关闭文件是利用close()进行的;

    对应的这个接口将传入一个文件描述符从而将文件进行关闭;

    int main() {char buf[1024];buf[1024] = 0;ssize_t ret = read(0, buf, sizeof(buf));if (ret > 0) {close(1);buf[ret] = '\0';fprintf(stdout, buf, strlen(buf));}return 0;
    }
    

    在这段代码当中使用了close()将文件描述符1对应的文件关闭并调用了fprintf()传入标准输出对缓冲区内的信息进行打印;

    运行结果如下:

    $ ./mytest 
    aaaaa
    $ 
    

    从结果可以看出,由于文件描述符1被关闭,对应的使用标准输出stdout进行输出时将不做反应;

    那么此时将标准输出对应的代码修改为标准错误的代码,即 fprintf(stderr, buf, strlen(buf));,其余部分不变时再次运行程序;

    $ ./mytest 
    aaaaaa
    aaaaaa
    

    从结果可以看出,当文件描述符1被关闭时调用fprintf()函数并以标准输出的方式进行打印时则无法输出,而由于文件描述符2对应的文件未被关闭故可以使用标准错误进行打印;

在上文中的例子当中验证了实际上三个文件描述符0,1,2对应着C语言当中的三个标准输入输出流;

与上文联系,可以得出三个文件描述符也对应着输入设备与输出设备;

  • 既然说 Linux当中一切皆文件 那么为什么使用close()将文件描述符1中的文件关闭为什么还能使用标准错误进行打印?为什么对应的硬件接口未被关闭?

在上文当中提到,在struct fiel结构体当中存储了许多与文件相关的信息;

其中的一个信息即为 引用计数 ;

  • 引用计数

    引用计数即可以判断当前资源是否有被哪个进程所使用;

    文件是可以被多个进程打开的,当调用open()函数打开某个文件时该文件的引用计数将会+1;

    对应的当调用close()函数将该文件关闭时,其引用计数将会-1;

    当引用计数变为0时,则表示该资源需要被进行回收;


👾 FILE*指针与文件描述符fd的关系

请添加图片描述

在上文当中介绍了FILE*指针与文件描述符fd;

那么为什么文件描述符是以int类型存在,而在C语言当中确实以一个名为FILE*指针进行区别;

实际上FILE类型是C语言封装出来的一种结构体类型;

而其要进行对应的文件操作意味着其必定需要调用操作系统的系统调用接口;

那么文件描述符fd是必不可少的元素;

可以推断出实际上在FILE这个类型的定义当中,主要的核心是依靠文件描述符fd进行的;

  • 那么如何能够证明?

    C++当中,由于类是可以被设为私有的,故若是在C++当中需要访问一个较为核心的成员是不被允许的(一般会被设置为私有或保护);

    而在C语言当中结构体是没有权限的,这意味着可以直接将对应的信息进行打印;

    int main() {printf("stdin->fd1:%d\n",stdin->_fileno);printf("stdout->fd2:%d\n",stdout->_fileno);printf("stderr->fd3:%d\n",stderr->_fileno);return 0;
    }
    

    对应的结果为:

    $ ./mytest 
    stdin->fd1:0
    stdout->fd2:1
    stderr->fd3:2
    

故实际上在上文当中的 “在操作系统当中,默认会打开三个标准输入输出流” 是不准确的;

由于 标准输入输出流 这个概念是在C语言层面的,而在OS层面只有文件描述符fd;

故这句话在OS层面应该为: “在操作系统当中,默认会打开三个文件描述符,分别为0,1,2

  • 那么为什么操作系统默认会打开这三个文件描述符?

在操作系统当中,操作系统为用户提供了一个抽象层的接口,而这个接口就是对应的C语言当中的标准输入输出流,也是操作系统当中的文件描述符;

同时由于他们是一个抽象层的接口所以并不需要用户进行思考;

实际上这三个文件描述符的打开本身与硬件是没有太大关系的;

OS的启动当中,OS将会默认去检测当前的硬件,这包括CPU,GPU,内存,输入输出设备等以保证这些硬件功能的良好同时判断自己可以使用哪些硬件;

而这个检测出来的硬件本身与三个文件描述符无关;

因为无论是否有检测到输入输出设备都会将三个文件描述符进行占用,因为即使没有检测到输入输出设备也可以通过其他的方式进行输入输出;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/7518.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【微积分】微分方程的求解(必看)

文章目录 微分方程1.一阶微分方程1.1 可分离变量型微分方程1.2 一阶齐次微分方程1.3 一阶线性微分方程 2. 高阶微分方程2.1 可降阶的高阶微分方程求解&#xff08;以二阶为例&#xff09;2.2 二阶常系数线性微分方程2.2.1 二阶常系数齐次微分方程2.2.2 二阶常系数非齐次微分方程…

【操作指南】银河麒麟高级服务器操作系统内核升级——基于4.19.90-17升级

1. 升级清单 升级包及依赖包清单如下。 kernel ARM架构 kernel-core-4.19.90-23.18.v2101.ky10.aarch64.rpm kernel-modules-4.19.90-23.18.v2101.ky10.aarch64.rpm kernel-4.19.90-23.18.v2101.ky10.aarch64.rpm kernel-modules-extra-4.19.90-23.18.v2101.ky10.aarch64.r…

ASV1000视频监控平台:接入支持JT808标准的设备

目录 一、JT/T 808标准简介 &#xff08;一&#xff09;概述 &#xff08;二&#xff09;标准内容简介 1、消息分类 2、位置信息 3、报警信息 4、车辆控制 5、数据转发 二、在ASV1000上通过JT808添加设备 &#xff08;一&#xff09;登录视频监控平台管理端 &#x…

Coze扣子开发指南:怎样建立一个工作流?

Coze扣子的工作流支持通过可视化的方式&#xff0c;对插件、大语言模型、代码块等功能进行组合&#xff0c;从而实现复杂、稳定的业务流程编排。​ 简单的说&#xff0c;目前阶段&#xff0c;单独靠大语言模型能完成的任务还是有限的&#xff0c;那怎么办呢&#xff1f;解决方案…

电机控制系列模块解析(17)—— 速度环

一、电机转速控制 电机控制的速度环是整个电机控制系统中的外环&#xff0c;其主要任务是根据设定的转速指令值&#xff08;目标速度&#xff09;与实际电机转速之间的偏差&#xff0c;调整电流环的参考值&#xff08;d轴电流Id或q轴电流Iq&#xff0c;涉及类似单电流环的弱磁…

抖音主播/电商人员有福了,利用Suno创作产品宣传,让产品动起来-小米Su7 - 第510篇

历史文章 AI音乐&#xff0c;8大变现方式——Suno&#xff1a;音乐版的ChatGPT - 第505篇 日赚800&#xff0c;利用淘宝/闲鱼进行AI音乐售卖实操 - 第506篇 如何让AI生成自己喜欢的歌曲-AI音乐创作的正确方式 - 第507篇 导读 现在不管是淘宝电商&#xff0c;还是抖音电商&a…

借助Aspose.SVG图像控件,在线将 PNG 转换为 Base64 字符串

Aspose.SVG for .NET 是用于SVG文件处理的灵活库&#xff0c;并且与其规范完全兼容。API可以轻松加载&#xff0c;保存和转换SVG文件&#xff0c;以及通过其文档对象模型&#xff08;DOM&#xff09;读取和遍历文件的元素。API独立于任何其他软件&#xff0c;使开发人员无需使用…

第10篇:创建Nios II工程之控制单个七段数码管

Q&#xff1a;还记得之前使用Verilog case语句来描述实现七段数码管的逻辑功能。本期我们创建Nios II工程用C语言代码实现相同的功能。 A&#xff1a;基本原理&#xff1a;一个七段数码管由7个发光二极管LED组成&#xff0c;所以控制一个数码管的显示即控制7个LED。我们在之前…

手把手教你上手开源性能监控神器Arthas

前言 在日常的工作中&#xff0c;对于商业项目尤其是并发量较高的项目&#xff0c;系统在一些情况下会莫名其妙把CPU打满并且导致服务宕机&#xff0c;虽然90%的情况下&#xff0c;是迭代发版的代码有bug&#xff0c;但是既然有这个情况&#xff0c;线上出现事故了&#xff0c…

海外大带宽服务器的带宽大小是如何定义的?

海外大带宽服务器的带宽大小通常是由提供的数据传输速率来衡量的。Rak部落小编为您整理发布海外大带宽服务器的带宽大小是如何定义的? 带宽的大小决定了服务器能够处理的数据量和传输速度&#xff0c;这对于确保服务器性能至关重要。在详细定义中&#xff0c;带宽可以根据以下…

对C语言符号的一些冷门知识运用的剖析和总结

符号 目录* 符号 注释 - 奇怪的注释 - C风格的注释无法嵌套 - 一些特殊的注释 - 注释的规则建议 反斜杠’’ - 反斜杠有续行的作用&#xff0c;但要注意续行后不能添加空格 * 回车也能起到换行的作用&#xff0c;那续行符的意义在哪&#xff1f; - 反斜杠的转义功能 单引号…

公钥私钥?一文搞懂非对称加密

非对称加密 非对称加密&#xff1a; 通信双方分别创建公钥和私钥&#xff0c;并且保证公钥所加密的信息&#xff0c;只有配对的私钥可以解密&#xff0c;接下来&#xff0c;双方公开交换公钥&#xff0c;通信时&#xff0c;使用对方的公钥进行加密&#xff0c;如此&#xff0…

国内小白用什么方法充值使用ChatGPT4.0?

首先说一下IOS礼品卡订阅&#xff0c;目前最经济实惠的订阅方式&#xff0c;具体操作步骤 使用IOS设备充值&#xff0c;用 App Stroe 兑换券 1、支付宝地址切换旧金山&#xff0c;在里面买app store 的兑换卷 2、美区Apple ID登陆app store &#xff0c;充值兑换券 3、IOS设…

JavaScript中的事件模型

JavaScript中的事件模型分为&#xff1a;事件和事件流、原始事件、标准事件和IE事件。 事件与事件流 JavaScript中的事件&#xff0c;可以理解为HTML文档或者浏览器中发生的一种交互操作&#xff0c;让网页有互动的功能。常见的事件就是加载事件、鼠标事件和自定义事件。 因…

Find My资讯|苹果设备在修复期间可以保持启用“Find My“功能

iOS 17.5 中有一项名为"维修状态"的新功能&#xff0c;可让送修的设备保持启用"查找我的"&#xff08;Find My&#xff09;功能。此前&#xff0c;用户在送修设备时必须禁用跟踪设备位置的"查找我的"功能&#xff0c;但iOS 17.5发布后&#xff0…

vscode远程免密ssh原理与实操方法

什么是SSH SSH是一种加密协议&#xff0c;全称为Secure Shell&#xff0c;用于安全地远程登录到服务器或其他远程设备上执行命令或传输文件。它提供了一种安全的加密通信机制&#xff0c;使得远程登录和文件传输等操作不会被恶意攻击者窃取或篡改&#xff0c;确保了数据的保密…

Jackson系统开发方法

1、Jackson分析方法是面向数据流的分析方法。这一方法从目标系统的输入、输出数据结构入手&#xff0c;导出程序框架结构&#xff0c;再补充其它细节&#xff0c;就可得到完整的程序结构图。 2、应用场景&#xff1a;这一方法对输入、输出数据结构明确的中小型系统特别有效&am…

数据库(MySQL)—— 初识和创建用户

数据库&#xff08;MySQL&#xff09;—— 初识 什么是数据库数据库的种类创建用户mysql -h 主机名或IP地址 -u 用户名 -p 登录mysqlSELECT USER(); 查看当前用户切换用户GRANT ALL PRIVILEGES ON 赋予用户权限 REVOKE 撤销权限示例注意事项 MySQL的图形化界面工具查看所有用户…

【oracle数据库安装篇三】Linux6.8单机环境oracle11g容灾ADG搭建

说明 DataGuard 是在主节点与备用节点间通过日志同步来保证数据的同步&#xff0c;可以实现数据库快速切换与灾难性恢复。用户能够在对主数据库影响很小的情况下&#xff0c;实现主备数据库的同步。 关联文章 【oracle数据库安装篇一】Linux5.6基于LVM安装oracle11gR2单机 【…

追踪攻击数据包中的真实IP地址:方法与技巧

在网络安全领域&#xff0c;追踪攻击数据包中的真实IP地址是一项至关重要的任务。通过确定攻击者的真实IP地址&#xff0c;可以有效地识别和阻止网络攻击行为&#xff0c;提高网络安全防御水平。IP数据云IP地址查询将介绍几种常用的方法和技巧&#xff0c;帮助安全人员有效追踪…