【Linux文件IO】系统IO中API描述和基本使用

发布于:2025-03-17 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、LInux文件IO的说明

1、文件的概念(LInux下一切皆文件)

\quad 在Linux系统语境下,文件(file)一般有两个基本含义:

  • 狭义:指普通的文本文件,或二进制文件。包括日常所见的源代码、word文档、压缩包、图片、视频文件等等。
  • 广义:除了狭义上的文件外,几乎所有可操作的设备或接口都可视为文件。包括键盘、鼠标、硬盘、串口、触摸屏、显示器等,也包括网络通讯端口(多机通信要用到的文件)、进程间通讯管道(单机同信)等抽象概念。

2、Linux系统中文件的分类

在Linux中,文件总共被分成了7种,他们分别是:
1.普通文件(popular) (符号:-): 存在于外部存储器中,用于存储普通数据。
2.目录文件(directory) (符号:d): 用于存放目录项,是文件系统管理的重要文件类型。
3.管道文件(pipeline) (符号:p): 一种用于进程间通信的特殊文件,也称为命名管道FIFO。
4.套接字文件(socket) (符号:s): 一种用于网络间通信的特殊文件。
5.链接文件(link) (符号:l): 用于间接访问另外一个目标文件,相当于Windows快捷方式。
6.字符设备文件(character) (符号:c): 字符设备在应用层的访问接口 (以字符为单位,跟系统进行数据交换的设备,比如:键盘、鼠标、触摸屏等)
7.块设备文件(block) (符号:b): 块设备在应用层的访问接口 (以块为单位(256字节,1024字节等),跟系统进行数据交换的设备,比如:u盘、内存、硬盘等)

3、系统IO与标准IO

对文件的操作,基本上就是输入输出,因此也一般称为IO接口。
在操作系统的层面上: 这一组专门针对文件的IO接口就被称为系统IO;
在标准库的层面上: 这一组专门针对文件的IO接口就被称为标准IO,如下图所示:
在这里插入图片描述

  • 系统IO:是众多系统调用当中专用于文件操作的一部分接口。
  • 标准IO:是众多标准函数当中专用于文件操作的一部分接口。
    从图中还能看到,标准IO实际上是对系统IO的封装,系统IO是更接近底层的接口。

4、如何选择系统IO与标准IO

  • 系统IO
    • 由操作系统直接提供的函数接口,特点是简洁,功能单一
    • 没有提供缓冲区,因此对海量数据的操作效率较低
    • 套接字Socket、设备文件的访问只能使用系统IO
  • 标准IO
    • 由标准C库提供的函数接口,特点是功能丰富
    • 有提供缓冲区,因此对海量数据的操作效率高
    • 编程开发中尽量选择标准IO,但许多场合只能用系统IO

总的来讲,这两组函数接口在实际编程开发中都经常会用到,都是基本开发技能。

二、系统IO的概念

1、概念:linux系统内核供的接口函数,用来对文件进行读写操作
2、如何查看权限掩码

  • 查看掩码值 :umask
> 0002   // 第一个0表示是八进制,后面三个数组才是掩码值
  • 修改掩码值 umask 新的掩码值
比如:umask  0022  // 掩码022表示要去掉同组用户的写权限,其他用户的写权限

三、系统IO基本API

1、文件描述符

在linux系统的头文件/usr/src/linux-headers-4.15.0-22/include/linux/sched.h中定义一个结构体

(1) 概念

\quad 函数 open() 的返回值,是一个整型 int 数据。这个整型数据,实际上是内核中的一个称为 fd_array 的数组的下标:
在这里插入图片描述
打开文件时,内核产生一个指向 file{} 的指针,并将该指针放入一个位于 file_struct{} 中的数组 fd_array[] 中,而该指针所在数组的下标,就被 open() 返回给用户,用户把这个数组下标称为文件描述符,如上图所示。

struct task_struct  //用来保存当前程序运行时候产生的状态信息
{
   当前程序打开的所有文件的属性信息
   struct files_struct  *files --->结构体数组的起始位置(该结构体数组每个成员分别存储打开的每一个文件的属性信息)
                                   文件描述就是该文件在数组中的下标位置
                                   struct files_struct  数组名[元素个数]
                                   struct files_struct  *files=数组名    
   当前程序占用的内存空间大小
   当前程序栈的使用情况
   当前程序堆的使用情况
   ......
}
  • 文件描述符从0开始,每打开一个文件,就产生一个新的文件描述符。
  • 可以重复打开同一个文件,每次打开文件都会使内核产生系列结构体,并得到不同的文件描述符
  • 由于系统在每个进程开始运行时,都默认打开了一次键盘、两次屏幕,因此0、1、2描述符分别代表标准输入(键盘)、标准输出(键盘)和标准出错(键盘)三个文件(两个硬件)。

在这里插入图片描述

(2) 文件描述符的分配规则:

  • 第一个:0 1 2已经默认被键盘,液晶屏占用了,只能从3开始分配
  • 第二个:linux系统总是把目前没有被占用的最小的文件描述符分配给你
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    int fd1, fd2, fd3;

    // 可读可写的权限打开1.txt
    fd1 = open("./1.txt", O_RDWR);
    if (fd1 == -1)
    {
        printf("open fd1 error!\n");
        return -1;
    }
    printf("fd1的文件描述符是%d\n",fd1);
    

    // 可读可写的权限打开2.txt
    fd2 = open("./1.txt", O_RDWR);
    if (fd2 == -1)
    {
        printf("open fd2 error!\n");
        return -1;
    }
    printf("fd2的文件描述符是%d\n",fd2);
    // 关闭fd1文件
    close(fd1);
    // 关闭fd2文件
    close(fd2);

    // 可读可写的权限打开3.txt
    fd3 = open("./3.txt", O_RDWR);
    if (fd1 == -1)
    {
        printf("open fd3 error!\n");
        return -1;
    }
    printf("fd3的文件描述符是%d\n",fd3);
    // 关闭fd3文件
    close(fd3);
    
    return 0;
}
/*
执行结果:
    fd1的文件描述符是3
    fd2的文件描述符是4
    fd3的文件描述符是3
*/

  • 第三个:文件描述符最大值是多少–》0—1023
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
/* 文件描述符最大值是多少--》0---1023 */
    int fd;
    for (int i = 0; i < 2000; i++)
    {
        fd = open("./1.txt", O_RDWR);
        if (fd == -1)
        {
            printf("open fd error!\n");
            return -1;
        }
        printf("文件描述符是%d\n",fd);
    }
    
    return 0;
}

在这里插入图片描述

2、文件的打开/新建 man 2 open

注:linux中所有的函数,只要参数需要用到多个宏定义,都是使用按位或连接起来

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 第一个版本的open
int open(const char *pathname, int flags);
       返回值:成功 --》返回文件描述符
                失败 --》返回-1
         参数:pathname --》你要打开的那个文件的路径名
               flags --》O_RDONLY   只读
                         O_WRONLY   只写
                         O_RDWR     可读写
                             情况一: 你想使用的权限跟文件本身的权限不一致,会打开失败(普通用户的身份运行程序,超级用户不受权限的约束)
                                     你想O_RDWR但是文件本身是只读/只写 --》会导致失败
                         O_APPEND   以追加的方式打开文件,打开文件的时候光标会自动到文件末尾
                         O_CREAT    新建文件
                         O_EXCL     跟O_CREAT配合使用,表示如果文件存在就失败退出,不新建,不存在就新建
                         O_TRUNC    跟O_CREAT配合使用,表示如果文件存在就清空覆盖掉原来的文件,不存在就新建
// 第二个版本的open    
int open(const char *pathname, int flags, mode_t mode);
         参数:mode --》你创建一个文件的时候,顺便给这个文件设置一个权限
                        使用的时候直接写个8进制的数来表示权限
                        写成0777,不要写成777(编译没有错误,修改结果不正确)
                        注:设置权限会受到umask掩码值的影响(之前的文件先手动删除,再使用O_TRUNC覆盖,但是权限改不了)
                            计算公式:你写的权限&(~umask) = 新建的文件最终的权限值
                            修改umask的值: umask 新的值

示例代码:

// 第一个版本的open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd;
    //新建一个记事本
    //O_EXCL表示文件存在就新建失败,不存在就自动创建
    //fd=open("./new.txt",O_CREAT|O_EXCL|O_RDWR); 
    //O_TRUNC表示文件存在就把源文件清空覆盖,不存在就自动创建
    fd=open("./new.txt",O_CREAT|O_TRUNC|O_RDWR);
    if(fd==-1)
    {
        printf("新建文件失败了!\n");
        return -1;
    }
    return 0;
}
// 第二个版本的open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd;
    //新建一个记事本
    //O_EXCL表示文件存在就新建失败,不存在就自动帮你创建
    //fd=open("/home/gec/new.txt",O_CREAT|O_EXCL|O_RDWR,0777); 
    //O_TRUNC表示文件存在就把源文件清空覆盖,不存在就自动帮你创建
    fd=open("/home/gec/new.txt",O_CREAT|O_TRUNC|O_RDWR,0777);
    if(fd==-1)
    {
        printf("新建文件失败了!\n");
        return -1;
    }
    return 0;
}

3、文件的读取 man 2 read

注: ssize_t和size_t都是用typedef给长整型取的别名

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);  
       返回值(重点):成功 --》返回成功读取到的字节数
                              如果一个文件读取完毕,再次调用read读取,read返回0个字节
                     失败 ---1
         参数:fd --》要读取的那个文件的文件描述符
               buf --》存放读取到的内容
               count --》读取多少字节的数据
                         如果count的值超过了文件中实际的字节数,read按照实际大小读取
          char buf[100]
          read(fd, buf, 451551545字节byte);   //导致数组溢出(段错误的风险)fd代表视频(500兆)

示例代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
    int fd;
    char buf[32] = {0}; // 存放读取到的内容
    
    // 以可读可写的权限打开1.txt,open打开默认光标在起始位置
    fd = open("./1.txt", O_RDWR);
    if (fd == -1)
    {
        printf("打开fd文件失败\n");
        return -1;
    }
    printf("fd的文件描述符是:%d\n", fd); // fd的文件描述符是:3
    // 从光标起始位置开始读取1.txt文件的内容
    // ssize_t ret = read(fd, buf, 5); // 读取到的文件内容是:12345, 读取到的字节数是:5
    // ssize_t ret = read(fd, buf, sizeof(buf)); // 读取到的文件内容是:1234567890, 读取到的字节数是:10
    ssize_t ret = read(fd, buf, 12); // 读取到的文件内容是:1234567890, 读取到的字节数是:10
    printf("读取到的文件内容是:%s, 读取到的字节数是:%ld\n", buf, ret);  
    
    // 再次读取
    ret = read(fd, buf, 12); // 读取到的文件内容是:1234567890, 读取到的字节数是:0
    printf("读取到的文件内容是:%s, 读取到的字节数是:%ld\n", buf, ret);  
    
    // 关闭文件
    close(fd);
    return 0;
}

4、文件的写入 man 2 write

ssize_t write(int fd, const void *buf, size_t count);
       返回值:成功:count写多少,返回值就是多少
               失败:-1
         参数:fd --》要写入的那个文件的文件描述符
               buf --》存放要写入的内容
               count --》打算写入多少字节的数据 
        write(fd,"hello",5) //返回值是5
        write(fd,"hello",10) //返回值是10,此时write发现hello不够10个字节,会找垃圾数凑够10个字节写入到文件中
        write(fd,"hello",100) //返回值是100,此时write发现hello不够100个字节,会找垃圾数凑够100个字节写入到文件中
        注:count的值不能乱写

示例代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
    write和read混合使用需要注意的问题
    读/写操作都会自动改变光标位置
    读/写永远都是从当前光标后面开始
*/

int main(int argc, char const *argv[])
{
    int fd;
    char *buf = "hello"; 
    int a = 12345;
    float b = 99.9;
    fd = open("./2.txt", O_RDWR);
    if (fd == -1)
    {
        printf("打开fd文件失败\n");
        return -1;
    }
    // 写入字符串
    write(fd, buf, strlen(buf));
    // 写入整数
    write(fd, &a, sizeof(a));
    // 写入浮点数
    write(fd, &b, sizeof(b));

    // 将文件指针重置到文件开头
    lseek(fd, 0, SEEK_SET);

    char buf1[10] = {0}; 
    int a1;
    float b1;

    // 读取字符串
    read(fd, buf1, strlen(buf));
    // 确保字符串以 '\0' 结尾
    buf1[strlen(buf)] = '\0';
    // 读取整数
    read(fd, &a1, sizeof(a1));
    // 读取浮点数
    read(fd, &b1, sizeof(b1));
    printf("buf1:%s\n", buf1);  // buf1:hello
    printf("a1:%d\n", a1);      // a1:12345
    printf("b1:%f\n", b1);      // b1:99.900002

    close(fd);
    
    return 0;
}

5、文件的关闭

int close(int fd);
      返回值:成功 0 失败  -1
        参数:fd --》你要关闭的文件描述符 

6、 获取文件的属性信息

获取大小,权限,类型…


int stat(const char *pathname, struct stat *buf);
      返回值:成功 0 失败  -1
        参数:pathname --》文件路径名
              buf --》结构体指针,用来存放获取到文件属性信息 
              struct stat
              {
                  mode_t    st_mode;       //保存文件的类型,权限
                  off_t     st_size;        //保存文件大小
              }
  用途一: 获取文件大小
            st_size 里面就是大小
  用途二:获取权限  (所代表的是八进制数)
            S_IRWXU  //宏定义 当前用户可读,可写,可执行
                 S_IRUSR  //宏定义 当前用户可读
                 S_IWUSR  //宏定义 当前用户可写
                 S_IXUSR  //宏定义 当前用户可执行
            S_IRWXG  //宏定义 同组用户可读,可写,可执行
                 S_IRGRP  //宏定义 同组用户可读
                 S_IWGRP  //宏定义 同组用户可写
                 S_IXGRP  //宏定义 同组用户可执行
            S_IRWXO  //宏定义 其他用户可读,可写,可执行
                 S_IROTH  //宏定义 其它用户可读
                 S_IWOTH  //宏定义 其它用户可写
                 S_IXOTH  //宏定义 其它用户可执行
            struct stat  mystat;
            if(mystat.st_mode&S_IRUSR)
                  printf("这个文件对于当前用户是可读的!\n");
  用途三:判断文件类型   (所代表的是八进制数)
                   S_IFMT     0170000
                   S_IFSOCK   0140000   套接字
                   S_IFLNK    0120000   软链接
                   S_IFREG    0100000   普通文件
                   S_IFBLK    0060000   块设备
                   S_IFDIR    0040000   目录
                   S_IFCHR    0020000   字符设备
                   S_IFIFO    0010000   管道

            写法一:与运算判断
                 switch(mystat.st_mode&S_IFMT) // 
                 {
                       case S_IFREG:  //普通文件
                             break;
                       case S_IFDIR:  //目录文件
                 }
                 注意:如果写成if条件判断,表达式要括起来防止优先级出问题
                       if((mystat.st_mode&S_IFMT)==S_IFREG)
            写法二:宏函数(带参数的宏定义)判断
                 S_ISDIR(m)  目录
                 S_ISREG(m)  普通文件
                 S_ISCHR(m)  字符设备
                 S_ISBLK(m)  块设备
                 S_ISFIFO(m) 管道
                 S_ISLNK(m)  软链接
                 S_ISSOCK(m) 套接字

                 if(S_ISREG(mystat.st_mode))
                       printf("这是个普通文件!\n");

示例代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    struct stat mystat;

    // 判断参数个数
    if (argc != 2)
    {
        printf("需输入参数!\n");
        return -1;
    }
    // 获取文件大小
    int ret = stat(argv[1], &mystat);
    if (ret == -1)
    {
        printf("获取文件属性失败\n");
        return -1;
    }
    
    printf("当前文件的大小:%ld\n", mystat.st_size);

    // 获取文件的权限
    if (mystat.st_mode & S_IRUSR)
    {
        printf("当前文件权限是可读的\n");
    }
    if (mystat.st_mode & S_IWUSR)
    {
        printf("当前文件权限是可写的\n");
    }
    if (mystat.st_mode & S_IXUSR)
    {
        printf("当前文件权限是可执行的\n");
    }
    // 获取文件类型-写法1
    // switch (mystat.st_mode & S_IFMT)
    // {
    // case S_IFLNK:
    //     printf("当前文件类型是软链接\n");
    //     break;

    // case S_IFREG:
    //     printf("当前文件类型是普通文件\n");
    //     break;
    
    // default:
    //     break;
    // }
    // 获取文件类型-写法2
    if(S_ISREG(mystat.st_mode))
        printf("普通文件!\n");

    printf("1.txt文件权限:%o\n", mystat.st_mode); // 1.txt文件权限:100777

    return 0;
}
/*
执行结果:
    当前文件的大小:10
    当前文件权限是可读的
    当前文件权限是可写的
    当前文件权限是可执行的
    普通文件!
    1.txt文件权限:100777
*/

在这里插入图片描述

7、修改文件的权限

int chmod(const char *pathname, mode_t mode);
       返回值:成功 0  失败 -1
         参数:pathname --》文件路径名
               mode --》八进制数,表示权限0777
         注:chmod命令修改权限以及chmod函数修改权限,都不需要考虑掩码umask
             不要在共享文件夹中修改权限,不准确
             open(新建一个文件,设置权限) 需要考虑计算掩码

8、判断文件是否存在

int access(const char *pathname, int mode);
        返回值:文件存在  0  不存在 -1
          参数:pathname --》文件路径名
                mode --》R_OK  //判断文件是否可读
                         W_OK  //判断文件是否可写
                         X_OK  //判断文件是否可执行
                         F_OK  //判断文件是否存在

示例代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    int ret = 0;
    // 判断参数个数
    if (argc < 2)
    {
        printf("需输入参数!\n");
        return -1;
    }
    
    if (access(argv[1], F_OK) == 0)
    {
        printf("%s文件存在\n", argv[1]);
        chmod(argv[1], 777);
    }
    else
    {
        printf("%s文件不存在\n", argv[1]);
        return -1;
    }

    return 0;
}

9、设置文件读写的偏移(设置光标)

作用:程序员可以依据自己的实际需要,往文件的任何位置读取/写入内容
off_t lseek(int fd, off_t offset, int whence);
      返回值:成功 返回当前偏移位置距离文件开头的字节数
              失败 -1
        参数:fd --》要设置读写偏移的文件的文件描述符
              offse --》要设置的读写偏移量,字节
              whence --》从文件的哪个位置开始偏移
                         SEEK_SET  //起始位置
                         SEEK_CUR  //当前位置,动态变化
                         SEEK_END  //末尾位置
    比如: lseek(fd,10,SEEK_SET); //把读写偏移设置成从起始位置往后偏移10个字节
用法一:
      lseek去设置读写偏移量
用法二:
      lseek求文件的大小
           lseek(fd,0,SEEK_END); // 利用返回值求文件大小
总结:
第一:画图分析
          最开始open打开(不能使用O_APPEND),光标从第一个字符前面开始画  
    第二:read还是write光标都会变动
    第三:lseek从哪里开始移动光标(看第三个参数和第二个参数)    
    第四:read/write都从光标后面一个位置开始

示例代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    char buf[12] = {0};
    int fd = open("./1.txt", O_RDWR); // 1234567890
    if (fd == -1)
    {
        printf("打开fd文件失败\n");
        return -1;
    }
    // 从起始位置往后偏移5位
    lseek(fd, 5, SEEK_SET); 
    // 读取文件
    // read(fd, buf, 5);   // 读取到的内容是:67890
    // printf("读取到的内容是:%s\n", buf); 

    // 光标偏移后写入
    write(fd, "nihao", 5);
    lseek(fd, -2, SEEK_CUR); 
    read(fd, buf, 5);   // 读取到的内容是:ao123
    printf("读取到的内容是:%s\n", buf); 

    lseek(fd, 17, SEEK_SET); 
    // 光标偏移后写入
    write(fd, "good", 4);

    // 可通过lseek的返回值计算文件的大小
    int size = lseek(fd, 0, SEEK_END);  
    printf("1.txt文件的大小是:%d\n", size); // 1.txt文件的大小是:21

    close(fd);

    return 0;
}

10、打印分析出错信息(文末附带错误码集)

#include <errno.h>
void perror(const char *s);
      参数:s --》打印信息
底层原理:
     linux系统把所有常见的错误类型用宏定义定义成一个个错误码
         文件不存在  1
         权限不够    2
         内存溢出    3
         #define ENOENT       2  /* No such file or directory */
         #define ESRCH        3  /* No such process */
         #define EINTR        4  /* Interrupted system call */
         ....
     linux的errno.h中有定义全局变量  int errno用来记录保存当前的错误码
     perror源码中依据errno中的错误码找到错误的具体原因

示例代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
    int fd = open("a.txt", O_RDWR);
    if (fd == -1)
    {
        // printf("打开fd文件失败\n");
        perror("打开fd文件失败\n");
        printf("errno的值是:%d\n", errno); // 
        return -1;
    }

    close(fd);
    return 0;
}

/*
替换成perror打印错误信息 此时输出
    打开fd文件失败
    : No such file or directory
    errno的值是:2
*/

11、字符串的拼接和拆分

int sprintf(char *str, const char *format, ...);  //变参函数(参数个数,类型不确定)
       参数:str --》存放拼接后的结果
             format --》打算按照什么格式去拼接数据

int sscanf(const char *str, const char *format, ...);
       参数:str --》你要拆分的字符串
             format --》打算按照什么格式去拆分字符

示例代码:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(int argc, char const *argv[])
{
    int year = 2025;
    char str[20] = "nihao";
    float dec = 99.9;
    // 定义数据存放拼接后的字符串
    char buf[100] = {0};
    // 拼接字符串   拼接好后的字符串 buf: nihao-2025-99.900002
    sprintf(buf, " %s-%d-%f", str, year, dec); // 
    printf("拼接后的字符串 buf:%s\n", buf);

    // 拆分字符串
    char date[128] = "2025-03-16-12.00";
    int y;
    int m;
    int d;
    float h;
    // 拆分后:2025#3#16#12.000000
    sscanf(date, "%d-%d-%d-%f", &y, &m, &d, &h);
    printf("拆分后:%d#%d#%d#%f\n",y, m, d, h);

    // 使用sscanf时,无法实现的方式
    char otherbuf[100] = "nihao@hello@world@666";
    char a1[10] = {0};
    char b1[10] = {0};
    char c1[10] = {0};
    char d1[10] = {0};

    /* 当需要拆分的数据全都是字符串时,sscanf会把%s当成一个整体,不会按照预定格式进行输出*/
    sscanf(otherbuf, "%s@%s@%s@%s", a1, b1, c1, d1);
    // 此时输出 :nihao@hello@world@666o@world@6666
    // printf("%s%s%s%s\n", a1, b1, c1, d1); 
    printf("%s\n", a1); 
    printf("%s\n", b1); 
    printf("%s\n", c1); 
    printf("%s\n", d1); 
    /*
    输出:
        nihao@hello@world@666
        o@world@666
        6


    */ 

    return 0;
}

12、 拆分字符串strtok(使用strtok不要使用sscanf)

char *strtok(char *str, const char *delim);  
       返回值:切割得到的字符串
               切割完毕,切割失败  返回NULL
         参数:str --》你要切割的字符串
               delim --》切割的标准(你打算用哪些字符作为字符串切割的分隔字符)
总结:拼接字符串sprintf
     切割拆分字符串strtok  

四、系统IO常用的API

1、概述

\quad 对文件的操作,除了最基本的打开、关闭、读、写、定位之外,还有很多特殊的情况,比如用于沟通应用层与底层驱动之间的ioctl、万能工具箱fcntl、内存映射mmap等等,熟练使用这些API,是日常开发的必备技能。

2、ioctl()

\quad 该函数是沟通应用层和驱动层的有力武器,底层开发人员在为硬件设备编写驱动的时候,常常将某些操作封装为一个函数,并为这些接口提供一个所谓的命令字,应用层开发者可以通过 ioctl() 函数配合命令字,非常迅捷地绕过操作系统中间层层机构直达驱动层,调用对应的功能。
\quad 从这个意义上讲,函数 ioctl() 像是一个通道,只提供函数调用路径,具体的功能由所谓命令字决定,下面是函数的接口规范说明:
在这里插入图片描述

  • 关键点:
    • request 就是所谓的命令字。
    • 底层驱动开发者可以自定义命令字。
    • 对于某些常见的硬件设备的常见功能,系统提供了规范的命令字。
  • 示例代码:
int main(void)
{
    // 打开一盏LED灯
    int led = open("/dev/Led", O_RDWR);

    // 通过命令字 LEDOP 及其携带的0/1参数,控制LED灯的亮灭
    // 此处,LEDOP 是底层开发者自定义的命令字
    ioctl(led, LEDOP, 1);
    ioctl(led, LEDOP, 0);

    // 打开一个摄像头
    int cam = open("/dev/video0", O_RDWR);

    // 通过命令字 VIDIC_STREAMON 及其携带参数 vtype 启动摄像头
    enum v4l2_buf_type vtype= V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(cam, VIDIOC_STREAMON, &vtype);
}

3、dup() 与 dup2()

  • dup 是英语单词 duplicate 的缩写,意即“复制”。
  • 这两个函数功能类似,都是用来“复制”文件描述符,接口规范如下:
    在这里插入图片描述

\quad dup()会将指定的旧文件描述符 oldfd 复制一份,并返回一个系统当前未用的最小的新文件描述符。注意,此时这新旧两个文件描述符是可以互换的,因为它们本质上指涉的是同一个文件,因此它们共享文件的读写偏移量和文件的状态标签,比如使用lseek()对新文件描述符修改文件偏移量,这个操作会同时影响旧文件描述符oldfd,再如,使用read()对新文件描述符读取文件部分内容后,可以继续对旧文件描述符读取后续内容。
\quad dup2()跟dup()几乎完全一样,不同的地方在于前者可以指定新文件描述符的具体数值,而不局限于系统当前未用描述符的最小值。这样一来,就可以通过dup2()指定一个已用的描述符,来达到重定向文件流的作用。

示例代码:

int main()
{
    // 打开文件 a.txt ,获得其文件描述符 fd1
    // 此处 fd1 就代表了这个文件及其配套的系统资源
    int fd1 = open("a.txt", O_RDWR);

    // 复制文件描述符 fd1,默认得到最小未用的文件描述符
    dup(fd1);

    // 复制文件描述符 fd1,并指派为 100
    dup2(fd1, 100);
}
  • 解析:

\quad 使用dup函数时,会自动分配当前未用的最小的文件描述符,如上述代码,由于进程默认打开了0、1、2作为标准输入输出,于是 fd1 就是3,新产生的文件描述符就是4,而 dup2 函数可以任意指定文件描述符的数值,如果指定的文件描述符已经有所指代,那么原先指代关系将会被替换。这种情况被称为“重定向”。
在这里插入图片描述

4、fcntl()

\quad 该函数的名字是 file control 的缩写,顾名思义,它可以用来“控制”文件,与 ioctl 类似,此处的 “控制” 含义广泛,具体内容由其第二个参数命令字来决定,fcntl 的接口规范如下:
在这里插入图片描述

  • 关键点:
    • fcntl 是个变参函数,前两个参数是固定的,后续的参数个数和类型取决于 cmd 的具体数值。
    • 第二个参数 cmd,称为命令字。
    • 命令字有很多,常用的如下:

在这里插入图片描述

  • 示例代码①:
// 获取指定文件fd的标签属性
int flag = fcntl(fd, F_GETFL);

// 在其原有属性上,增添非阻塞属性
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
  • 示例代码②:
// 将套接字sockfd的信号属主设置为本进程
fcntl(sockfd, F_SETOWN, getpid());

\quad 在网络编程中,当一个套接字处于异步通信状态并收到一个远端的数据时,就会使得内核产生一个信号SIGIO,此时我们可以通过上述fcntl()技巧告诉内核这个信号的接收者。一般而言,接收者收到该信号后,就知道套接字上有数据等待处理了。

5、mmap()

(1) mmap函数使用

\quad 该函数全称是 memory map,意为内存映射,即将某个文件与某块内存关联起来,达到通过操作这块内存来间接操作其所对应的文件的效果。

在这里插入图片描述

  • 关键点:
    • mmap函数的flags参数是有很多的,上表只罗列了最简单的几个,详细信息请使用 man 手册进行查阅。
    • mmap函数理论上可以对任意文件进行内存映射,但通常用来映射一些比较特别的设备文件,比如液晶屏LCD。

注意:
\quad 在较旧的Linux内核(如2.6内核)中,可以直接使用mmap()来给LCD设备映射内存,但在较新Linux内核(如4.4内核)中,则需要经由DRM统一管理,不可直接mmap映射显存。

int main()
{
    // 以只读方式打开一个文件
    int fd = open("a.txt", O_RDWR);

    // 申请一块大小为1024字节的映射内存,并将之与文件fd相关联
    char *p = mmap(NULL, 1024, PROT_READ|PROT_WRITE,
                MAP_SHARED, fd, 0);

    // 将该映射内存的内容打印出来(即其相关联文件fd的内容)
    printf("%s\n", p);

    // 通过操作内存,间接修改了文件内容
    p[0] = 'x';
    printf("%s\n", p);
    
    // 解除映射
    munmap(p, 1024);
    return 0;
}

注意几点:
1.使用 mmap 映射内存时,指定的读写权限必须是打开模式的子集。
2.映射模式为 MAP_SHARED 时,修改的内容会影响文件。
3.映射模式为 MAP_PRIVATE 时,修改的内容不会影响文件。

附件:perror错误码集(133-255为Unknown error)

在这里插入图片描述