Linux--基础IO

news2024/11/18 4:22:04

目录

C文件IO

系统文件IO

接口介绍

系统调用和库函数

文件描述符

open返回值

文件描述符的分配规则

 重定向

代码演示

使用dup2系统调用

缓冲区

FILE

理解文件系统

文件系统

 inode 

 软硬链接

静态库和动态库

概念

生成静态库

生成动态库 

C文件IO

  • 写文件
    #include <string.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    int main()
    {
            FILE *fp = fopen("log.txt","w");
            if(!fp)
            {
                    perror("fopen");
            }
    
            char buf[1024];
            const char *msg = "hello world\n";
    
            int count = 5;
            while(count--)
            {
                    fwrite(msg,strlen(msg),1,fp);
            }
    
            fclose(fp);
            return 0;
    }

    打开log.txt查看:

  •  读文件
    #include <string.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    int main()
    {
       FILE *fp = fopen("log.txt","r");
       if(!fp)
       {
            perror("fopen");
       }
    
       char buf[1024];
       const char *msg = "hello world\n";
    
       while(1)
       {
            ssize_t s = fread(buf,1,strlen(msg) ,fp);
            if(s > 0)
            {
                 buf[s] = 0;
                 printf("%s",buf);
            }
            if(feof(fp))
            {
                 break;
            }
        }
    
        fclose(fp);
        return 0;
    }

    运行结果:

 输出

#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{

     const char *msg = "hello world\n";
     fwrite(msg,strlen(msg),1,stdout);

     printf("hello printf\n");
     fprintf(stdout, "hello fprintf\n");
     return 0;
}

  •  stdin & stdout & stderr
  1. C默认会打开三个输入输出流,分别是stdin,stdout,stderr
  2. 这三个流的类型都是FILE*,fopen返回值类型,文件指针
  • 总结--打开文件的方式 
文件使用方式含义
"r"--只读打开一个已经存在的文件,可读
"r+"--读写打开一个文件,可读可写
"w"--只写打开一个文件,不存在可创建新文件,可写
"w+"--读写打开一个文件,不存在可创建新文件,可读写

"a"--追加

向文件末尾追加数据,不可读
"a+"--读写向文件末尾追加数据,可读写

系统文件IO

操作文件,除了上述C接口,还可以采用系统接口来进行文件访问。先实现和上边一样的代码。

  •  写文件

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

int main()
{
        umask(0);
        int fd = open("log.txt",O_WRONLY|O_CREAT,0666);
        if(fd < 0)
        {
                perror("open");
                return 1;
        }

        int count = 5;
        const char *msg = "hello world\n";
        int len = strlen(msg);

        while(count--)
        {
                write(fd,msg,len);
        }

        close(fd);
        return 0;

}

  • 读文件 
    
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <string.h>
    int main()
    {
            int fd = open("log.txt",O_RDONLY);
            if(fd < 0)
            {
                    perror("open");
                    return 1;
            }
    
            const char *msg = "hello world\n";
            char buf[1024];
            while(1)
            {
                    ssize_t s = read(fd,buf,strlen(msg));
                    if(s > 0)
                    {
                            printf("%s",buf);
                    }
                    else
                    {
                            break;
                    }
            }
            close(fd);
            return 0;
    }
    

    接口介绍

  •  open接口
#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);
  • 参数解释
  • pathname:要打开或创建的目标文件
  • flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。

        参数

  1. O_RDONLY:只读打开
  2. O_WRONLY:只写打开
  3. O_EDWR:读写打开 (1,2,3必须指定一个且只能指定一个)
  4. O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限。
  5. O_APPEND:追加写
  • mode_t:需要进行创建文件时,就需要指定创建文件的八进制访问权限值。
  • 返回值:成功返回新打开的文件描述符,失败返回-1。

open函数具体使用哪个,和具体的应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

系统调用和库函数

  • fopen、fclose、fread、fwrite都是C标准库当中的函数,这些为库函数

  • open、close、read、write都是系统提供的接口,称为系统调用接口。 

系统调用接口和库函数的关系,如下图所示:

文件描述符

open返回值

  • 文件描述符fd

文件描述符就是一个小整数

  • 0&1&2
  • Linux进程默认情况下会有三个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2
  • 0,1,2对应的物理设备一般是:键盘、显示器、显示器

所以输入输出还可以采用如下方式:

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

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

     return 0;
}

文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*file,指向一张表file_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针,所以,本质上,文件描述符就是该数组的下标。所以只要拿着文件描述符,就可以找到对应的文件。

描述如图:

 文件描述符的分配规则

规则:从头遍历数组fd_array[],找到一个最小的并且没有被使用的下标,分配给新的文件。

 代码演示:

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

int main()
{
    int fd1 = open("log.txt",O_RDONLY);
    int fd2 = open("log.txt",O_RDONLY);

    printf("fd1: %d\n",fd1);
    printf("fd2: %d\n",fd2);

    close(fd1);
    close(fd2);
    return 0;
}

从结果可以看出输出是从3开始的进行分配的。当关闭0和2,代码如下: 

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

int main()
{
        close(0);
        close(2);
        int fd1 = open("log.txt",O_RDONLY);
        int fd2 = open("log.txt",O_RDONLY);

        printf("fd1: %d\n",fd1);
        printf("fd2: %d\n",fd2);

        close(fd1);
        close(fd2);
        return 0;

}

我们可以看到结果是,fd1: 0,fd2 : 1。可见,文件描述符的分配规则是:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。 

 重定向

代码演示

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

int main()
{
        close(1);
        int fd = open("log.txt",O_WRONLY|O_CREAT,00644);
        if(fd < 0)
        {
            perror("open");
                return 1;
        }

        printf("fd: %d\n", fd);
        fflush(stdout);
        close(fd);
        exit(0);
        return 0;
}

此时,我们可以发现,本来应该输出到显示器上的内容,输出到了文件myfile当中,其中,fd=1。这种现象就叫做输出重定向。我们常见的重定向有:

  • >:命令的结果输出到文件上
  • >>:命令的结果追加的方式输出到文件上
  • <  1 > 2:讲文件1作为命令的输入设备,讲执行结果输出到文件2中
  •  重定向的本质

如果我们要进行重定向,上层只认0,1,2,3这样的fd。我们可以在OS内部通过一定的方式调整数组的特定下标的内容(指向),我们就可以完成重定向操作

示图:

 使用dup2系统调用

  • 函数原型
#include <unistd.h>
int dup2(int oldfd, int newfd);
  • 代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
        int fd = open("log.txt",O_CREAT | O_RDWR);
        if(fd < 0)
        {
                perror("open");
                return 1;
        }
        close(1);
        dup2(fd,1);

        char buf[1024] = {0};
        ssize_t read_size = read(0,buf,sizeof(buf)-1);
        if(read_size < 0)
        {
                perror("read");

        }
        printf("%s",buf);
        fflush(stdout);
        return 0;
}

dup2是让newfd赋值oldfd对应的描述信息,成功则关闭newfd原来的指向,也可以简单理解:dup2接口就是让两个描述符都指向oldfd所指向的文件,相当于是对newfd进行了重定向。 

缓冲区

  • 什么是缓冲区

缓冲区的本质就是一段内存 

  • 为什么要有缓冲区
  1. 解放使用缓冲区的进程时间
  2. 缓冲区的存在可以集中处理数据刷新,减少IO的次数从而达到提高整体的效率的目的 
  • 缓冲区在哪里

先来感受一段代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
        printf("hello printf");
        const char *msg = "hello write";
        write(1,msg,strlen(msg));
        sleep(5);
        return 0;
}

 

我们可以从结果中看出,printf并没有立即刷新,这就是因为有缓冲区的存在,printf内部封装了write。但是write是立即刷新的,所以通过上边的代码演示,我们虽然还不知道缓冲区在哪里,但是我们可以肯定缓冲区是不在write内部。而write是系统接口,所以我们讨论的缓冲区,并不是内核级别的。所以可以看出这个缓冲区只能是C语言提供的,是语言级别的缓冲区。

printf是C库当中的IO函数,一般往stdout中输出,stdout是FILE*类型,FILE*是一个struct结构体,会封装很多的属性,其中有fd还有该FILE对应的语言级别的缓冲区。

图示:

我们写的代码不是直接写在操作系统里边的,而是写到语言级别的缓冲区中(cache),等到数据量积累到一定程度时,会定期通过fd调用write(),把数据刷新到对应的内存当中。但是存在一个问题,什么时候去刷新,这里有一个刷新策略如下介绍:

  • 刷新策略
  • 无缓冲:立即刷新,系统调用接口就是无缓冲
  • 行缓冲:逐行进行刷新
  • 全缓冲: 缓冲区数据满时进行刷新

 FILE

  • 因为IO相关函数与系统调用接口对应,并且库函数封装了系统调用,所以本质上,访问文件都是通过fd访问的。
  • 所以C库当中的FILE结构体内部,必定封装了fd

研究一下如下代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main()
{
        const char *msg0 = "hello printf\n";
        const char *msg1 = "hello fwrite\n";
        const char *msg2 = "hello write\n";

        printf("%s",msg0);
        fwrite(msg1,strlen(msg0),1,stdout);
        write(1,msg2,strlen(msg2));

        fork();
        return 0;
}

运行结果: 

对进程实现重定向: 

 我们发现printf和fwrite(库函数)都输出了2次,而write只输出了一次(系统调用)。这是为什么呢?

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
  • printf、fwrite库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
  • 我们被放在缓冲区的数据不会被立即刷新,在进程退出的时候统一进行刷新,写入到文件中
  • 但是fork的时候,父子数据会发生写时拷贝,所以当父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
  • write没有变化,说明没有所谓的缓冲。

综上:printf、fwrite库函数会自带缓冲区,而write系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级别的缓冲区。其实为了提高整机性能,OS也会提供相关的内核级缓冲区。 

理解文件系统

文件系统

  • 使用ls-l查看文件信息

显示的信息中,每行有7列信息,分别是:模式、硬链接数、文件所有者、组、大小、最后修改时间、文件名。

  • ls-l读取存储在磁盘上的文件信息,然后显示出来,如图所示:

  •  除了使用ls-l方式读取信息,还有一个stat命令可以看到更多的信息

 inode 

为了解释清楚inode先介绍文件系统

上图是磁盘文件系统图,磁盘分区被划分为一个个block,block的大小是由格式化的时候确定的,并且不可以更改。

  • 对上图解释
  • Block group:文件系统会根据分区的大小划分为数个Block group,每个都有着相同的结构组成
  • 超级块(Super Block):存放文件系统本身的结构信息
  • GDT(Group Descriptor Table):块组描述符(有多少inode,起始inode编号,有多少inode被使用,有多少block被使用)
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
  • inode位图(inode Bitmap):每个bit表示一个inode是否空间可用
  • i节点表(inode Table):存放文件属性。如文件大小,所有者,最近修改时间等 
  • 数据区(Data blocks):存放文件内容

inode就是包含文件的所有属性,直接保存的就是该文件对应的blocks编号 

  • 当我们创建一个文件,操作系统做了什么呢?

操作系统修改inode Bitmap,在inode Table里找到对应inode结点,向对应inode Table里写文件的属性并且分配数据块,把数据写在数据块里同时修改Block Bitmap并且建立inode和block的映射关系,最终返回该文件的inode。

 软硬链接

  • 硬链接
  • 硬链接就是单纯的在Linux指定的目录下,给指定的文件新增文件名和inode编号的映射关系。
  • 硬链接数本质就是该文件inode属性中的一个计数器,count,标识有几个文件名和我的inode建立映射关系,简言之就是有多少个文件名指向我的inod。
  • 当我们删除文件时:1、在目录中将对应的记录删除。2、将硬链接数-1,如果为0,则将对应的磁盘释放。
  • 使用ln创建硬链接 

  •  软链接
  • 软链接是通过inode引用另一个文件,软链接是通过名字引用另一个文件,其实本质就是linux下的一个快捷方式。
  •  使用ln -s创建软链接

  •  软硬链接的区别

软链接是一个独立文件,有自己独立的inode和inode编号,硬链接不是一个独立文件,他和目标文件使用的是同一个inode。

  • 为什么创建普通文件,硬链接数默认是1?

因为普通文件的文件名,本身就和自己的inode具有映射关系,只有一个。 

静态库和动态库

概念

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间

 生成静态库

  • 代码示例

  • Makefile文件 

 

  •  运行结果

  •  运行测试文件

生成动态库 

  • 代码与静态库相同修改其Makefile
  • Makefile文件

  • 运行结果

  •  运行测试文件

 运行动态库有三种方法:

  1. 拷贝.so文件到系统共享库路径下,一般指/usr/lib
  2. 更改LD_LIBRARY_PATH(我上边用的就是这种方法)
  3. ldconfig配置/etc/ld.so.conf.d/(内部其实就是一个路径),使用ldconfig命令更新配置 

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

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

相关文章

SpringBoot+Prometheus+Grafana 实现自定义监控

1.Spring Boot 工程集成 Micrometer 1.1引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency><groupId>io.micrometer<…

E1. Erase and Extend (Easy Version)(纯暴力+string)

Problem - 1537E1 - Codeforces 这是该问题的简单版本。唯一的区别是对n和k的约束。只有当所有版本的问题都解决了&#xff0c;你才能进行黑客攻击。 你有一个字符串s&#xff0c;你可以对它进行两种类型的操作。 删除字符串的最后一个字符。 复制字符串&#xff1a;s:ss&…

数据结构---堆排序

堆排序JAVA实现和快速排序区别二叉堆的构建&#xff0c;删除&#xff0c;调整是实现堆排序的基础之前博客写了二叉堆&#xff1a; 二叉堆最大堆的堆顶是整个堆中的最大元素。最小堆的堆顶是整个堆中的最小元素。 堆排序步骤&#xff1a; 把无序数组构建成二叉堆。(需要从小到…

ArcGIS基础:等高线数据生成栅格DEM数据

以下操作为生成栅格DEM数据的方法。 一般方法是先创建TIN&#xff0c;然后在转为栅格DEM数据。 原始数据如下&#xff1a;为等高线数据&#xff0c;创建TIN数据需要用到等高线数据的【高程】字段。 声明&#xff1a;数据来源于网络。 工具位于【3D分析工具】下的【TIN】下…

BeanDefinition

1. 前言 Spring最重要的一个概念当属Bean了&#xff0c;我们写的Controller、Service、Dao凡是加了对应注解交给Spring管理的&#xff0c;都是Spring容器中的一个Bean。把我们自己写的类变成一个Bean交给Spring管理有很多的好处&#xff0c;比如我们不用自己去new对象了&#…

ssh+mysql实现的Java web企业人事人力资源管理系统源码+运行教程+参考论文+开题报告

今天给大家演示的是一款由sshmysql实现的Java web企业人事人力资源管理系统&#xff0c;其中struts版本是struts2&#xff0c;本系统功能非常完善&#xff0c;已经达到了可以商用的地步&#xff0c;基本全部实现了整个人力资源管理的所有功能&#xff0c;包括员工档案信息、部门…

jekins集成部署

jekins集成部署1.jekins简介2.Jenkins部署环境3. jekins安装4.配置jekins启动和停止脚本5.插件安装5.1.安装maven插件安装5.2 安装gitee插件5.3 安装Publish Over SSH插件5.4 安装 事件机制插件6.任务构建6.1 构建任务6.2 配置giteeApi令牌6.3 配置gitee源码地址6.4 在build中配…

3D激光里程计其二:NDT

3D激光里程计其二&#xff1a;NDT1. 经典NDT2. 计算方式2.1 2D场景求解:2.2 3D场景求解&#xff1a;3. 其他 NDTReference:深蓝学院-多传感器融合 1. 经典NDT NDT 核心思想&#xff1a;基于概率的匹配。目标是将点集 Y 匹配到固定的点集 X 中。这里的联合概率说的是将 X 划分成…

计算机毕业设计springboot+vue社区疫情防控系统

项目介绍 本系统运用最新的技术springboot框架,此框架是现在社会公司生产中所用的必需框架,非常实用,相比于以前的ssm框架,简单很多。前端框架运用vue框架,vue框架是最近几年非常流行的前端技术,适合很多开发语言。主要可以是系统的前端和后端进行解耦,分离,有利于开发者分别注…

设计模式——中介者模式

中介者模式一、基本思想二、应用场景三、结构图四、代码五、优缺点优点缺点一、基本思想 定义一个中介对象来封装一系列对象之间的交互&#xff0c;使原有对象之间的耦合松散&#xff0c;且可以独立地改变它们之间的交互。中介者模式又叫调停模式&#xff0c;它是迪米特法则的…

Proteus8仿真:51单片机LCD1602显示

51单片机LCD1602显示元器件原理图部分代码main.c工程文件元器件 元器件名称排阻RESPACK-851单片机AT89C51LCD1602LM016L按键BUTTON 原理图部分 LCD1602驱动: HD44780显示主要有8位操作8位两行显示&#xff0c;4位操作8位一行显示&#xff0c;8位操作8位一行显示。 LCD1602主要…

【Python】循环语句

目录 1.while 循环 2. for 循环 3. continue 4. break 1.while 循环 基本语法格式 while 条件&#xff1a; 循环体 条件为真&#xff0c;则执行循环体代码 条件为假&#xff0c;则结束循环 例1&#xff1a;打印 1 - 10 的整数 num 1 while num < 10:print(num)num 1 …

【AI with ML】第 10 章 :创建 ML 模型以预测序列

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

MySQL底层索引

目录 一、什么是索引&#xff1f; 二、MySQL索引结构分析【MySQL底层采用的是BTree】 1、为什么不使用二叉树&#xff1f; 2、为什么不使用红黑树&#xff1f; 3、为什么不使用Hash&#xff1f; 4、BTree与B-Tree的区别&#xff1f; 三、MySQL数据库的表结构、索引、数据 1、M…

基于Sharfetter-Gummel和改进的Sharfetter-Gummel计算对流扩散方程的通量(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

1. Arthas的命令

万恶淫为首&#xff0c;百善孝为先 Arthas命令 可以查看相应的文档: https://arthas.aliyun.com/doc/命令.html 如: https://arthas.aliyun.com/doc/grep.html https://arthas.aliyun.com/doc/cat.html 基础命令 help 查看命令帮助信息 cat 查看当前arthas 系统中的任意文件…

【算法】八月算法打卡

2022-08-01 低配版 promise class MyPromise {constructor(executor) {// 成功回调队列this._resolveQueue [];// 失败回调队列this._rejectQueue [];let resolve (val) > {while (this._resolveQueue.length) {const callback this._resolveQueue.shift();callback(v…

【iOS】对象,消息,运行期

文章目录对象&#xff0c;消息&#xff0c;运行期属性属性特质原子性方法名在对象内部尽量直接访问实例变量对象等同性特定类所具有的等同性判断等同性判定的执行深度容器中可变类的等同性以“类族模式”隐藏实现细节创建类族Cocoa里的类族在既有类中使用关联对象存放自定义数据…

原型,原型链,原型的继承

原型的作用? 1.节省内存空间 2.实现数据共享(继承) 什么是原型? 任何一个函数都有propotype属性,它本身是一个对象,我们称之为原型 构造函数,实例化对象与原型之间的关系? 1.任何一个函数都有prototype属性,它本身是一个对象,我们称之为原型 2.构造函数也是函数,也都…

C语言中头文件(.h)编写,头文件的包含<>和““很大不同

头文件书写技巧 1、头文件主要作用&#xff0c;提前声明函数&#xff0c;因为在c语言中必须先声明或定义才能使用 2、在使用到.c文件中需要把这个.h文件进行包含格式是#include <h文件>或#include "h文件名"&#xff0c;但**强烈建议用引号**&#xff0c;尖括…