第六章 结构体和内核IO

200 阅读10分钟

在C 语言中有基本数据类型 如 int、long、short、char、float、double等,还用用来存储一组相同类型的数组,要是有多个不同数据类型该如何体现呢?C 给我们提供了结构体,就是用来存放多个不同数据类型的集合。学习一门语言自然是少不了IO的处理,今天也和大家一起看看内核IO是什么,在C 中如何使用的。

结构体

概念:在C语言中有结构体这个变量。数组是多个相同类型的数据的集合,而结构体则是多个不同类型的数据的集合。本质上是一种分配内存的手段。和Java 中的类类似。

// 定义结构体
typedef struct Stu
{
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
} Stu;

struct Point
{
    int x;
    int y;
    char c[100];
} Point;

struct Node {
    int data;
    struct Point p;
    struct Node* next;
}n1 = {11, {5,6}, NULL};// 赋值



void print(struct Stu* ps)
{
    printf("name = %s age = %d\n", (*ps).name, (*ps).age);//调用结构体
    printf("name = %s age = %d\n", ps->name, ps->age);
}

int test_struct_exam1()
{
    //声明结构体变量
    struct Node n2 = {20, {5, 6}, NULL};
    printf("size:%lu \n", sizeof(n2));
    //创建结构体
    struct Stu s = {"lisi", 18};
    print(&s);
    return 0;
}

结构体当做参数传递,可以直接传引用对象,也可以传指针:

struct S
{
    int data[1000];
    int num;
};

struct S s = {{1,2,3,4}, 1000};

void print1(struct S s)
{
    printf("value:%d   |    size:%d    |    address:%p\n", s.num,sizeof(s),&s);
}

void print2(struct S* ps)
{
    printf("value:%d   |    size:%d     |    address:%p\n", ps->num,sizeof(ps),&ps);
}
int test_struct_exam2()
{
    print1(s);
    print2(&s);
    return 0;
}

IO与内核

linux下的IO: linux下一切皆文件,文件描述符是内核为了高效的管理已经被打开的文件所创建的索引,它是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都是通过文件描述符完成的。

inux下文件访问流程:

  • 内核=一套软件,操作系统用于支撑基础使用的功能程序数据的操作的真大哥
  • APP=上层应用--》很多基础功能是需要调用内核去进行完成
  • APP与内核的关系,不管是C还是JAVA, 以高级语言为主体开发的功能, 都有基础功能依赖于内核进行完成

image.png

image.png

文件描述符和打开文件之间的关系:

  • 每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。
  • 相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。
  • 系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件

内核维护的3个数据结构:

  • 文件描述符表:进程级的文件描述符表。
  • 打开文件表,系统级的打开文件描述符表,打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成包括当前文件位置,引用计数(即当前指向该表项的描述符表项数),以及一个指向 v-node 表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,知道它的引用计数为零。
  • i-node表:文件系统的i-node表,同文件表一样,所有的进程共享这张 v-node 表。每个表项包含 stat 结构中的大多数信息,包括 st_mode 和 st_size 成员

image.png 解释一下:

  • 在进程A中,文件描述符1和30都指向了同一个打开的文件句柄(标号23)。这可能是通过调用dup()、dup2()、fcntl()或者对同一个文件多次调用了open()函数而形成的。
  • 进程A的文件描述符2和进程B的文件描述符2都指向了同一个打开的文件句柄(标号73)
  • 这种情形可能是在调用fork()后出现的(即,进程A、B是父子进程关系),或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程时,也会发生
  • 再者是不同的进程独自去调用open函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
  • 此外,进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表的相同条目(1976),换言之,指向同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了open()调用。同一个进程两次打开同一个文件,也会发生类似情况

了解完linux下文件访问流程,下面就看看系统IO与标准IO。

系统IO与标准IO

C 语言不仅提供了访问顶层的函数,也提供了底层(OS)调用来处理存储设备上的文件。

  • 标准IO

    • 打开文件:fopen( ) 函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE的一个对象,类型 FILE 包含了所有用来控制流的必要的信息。
      • 函数原型:FILE *fopen( const char * filename, const char * mode )
      • mode:image.png
      • 如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"
    • 关闭文件:使用fclose( ) 函数。int fclose( FILE *fp );如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际 上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。
    • 写入文件:使用 int fputc( int c, FILE *fp );函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功,它会返回写入的字符,如 果发生错误,则会返回 EOF。
    • 读取文件:
      • int fgetc( FILE * fp );fgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回EOF。
      • char *fgets( char *buf, int n, FILE *fp );函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在 最后追加一个 null 字符来终止字符串。
      • nt fscanf(FILE *fp, const char *format, ...),在遇到第一个空格和换行符时,它会停止读取。
  • 系统IO

    • 打开文件:open();
      • 函数:
        • int open(const char * pathname, int flags);
        • int open(const char * pathname, int flags,mode_t mode);
        • 参数说明:
          • pathname --> 路径+名字
          • flags:O_RDWR 以可读写方式打开文件,利用 OR (|) 运算符组合;O_CREAT 若欲打开的文件不存在则自动建立该文件。
          • mode:只要在文件需要创建的时候生效,其余的情况无效,用来表示文件创建时的初始权限值。0654 --> 0 八进制;6 拥有者可读+可写;5 同组用户可读+可执行;4 其他用户可读;或者使用:S_IRUST|S_IWUSR|S_IRGRP|S_IXGRP|S_IROTH
        • 返回值:成功 :返回一个文件描述符(new file descriptor);nt --> 整型-->数字-->编号-->文件;失败 : 返回-1,错误号码会被设置
    • 写入信息:write();
      • 函数:size_t write (int fd, const void *buf , size_t count);
      • 参数
        • fd --> 需要写入的文件的描述符
        • buf --> 需要写入数据的首地址
        • count --> 需要写入的字节数
      • 返回值:返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入 errno 中
    • 读取信息:read();
      • 函数:ssize_t read(int fd, void *buf, size_t count);
      • 参数:
        • 第一个参数(fd):被读取的文件的文件描述符(即用open函数打开文件时获取到的文件描述符)
        • 第二个参数(buf):读取到的数据存放在这个变量中
        • 第三个参数(count):读取的数据字节数
      • 返回值:返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入 errno 中。
    • 关闭文件:close();
      • 函数:int clost(int fd);
      • 参数:fd --> 需要关闭的文件描述符
      • 返回值:若文件顺利关闭则返回 0    发生错误返回 -1 来个例子:

    读文件:

#include <stdio.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <unistd.h>  
#include <malloc.h>  
  
  
int main (int argc , char const *argv[])  
{  
    //第一步打开文件  
    int fd = open("./123.txt",O_RDONLY); //以只读的形式打开当前目录下的1.txt文件  
  
    //第二步:获取文件大小  
    int len = lseek(fd,0,SEEK_END); //先将指针偏移到文件末尾,获取文件末尾指针位置,即可获得文件大小  
  
    //第三步:将文件指针偏移到文件开头  
    lseek(fd,0,SEEK_SET);  
  
    //第四步:创建一个缓冲区来存放读取到的数据  
    char buf[len]; //创建一个大小与要读取的文件大小相等的字符数组来存放读取到的数据  
  
    //第五步:读取数据,将读取到的数据存放在数组中  
    read(fd,buf,len);  
  
    //第六步:打印读取到的数据  
    for(int i = 0; i < len; i++)  
    {  
        printf("%c",buf[i]);  
    }  
  
    //第七步:关闭文件  
    close(fd);  
    return 0;  
}
   

写文件:

#include <stdio.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <unistd.h>  
  
  
int main (int argc , char const *argv[])  
{  
    char *FileName = "./123.txt";  
    int fd_test = open (FileName, O_WRONLY | O_CREAT, 0666);  
    if(-1 == fd_test)  
    {  
        printf("不能打开文件:%s,原因是%d\n", FileName, strerror (0));  
        return -1;   // 返回值 -1 ,不然会报错  
    }  
    else  
    {  
        //第一步:打开文件  
        int fd = open("./123.txt",O_WRONLY | O_CREAT | O_TRUNC, 0777); //以只写的形式打开文件,文件存在则清空,不存在则创建  
  
        //第二步:将要写入的数据存放在字符串中  
        char* buf = "helloworld";  
  
        //第三步:获取字符串大小  
        int len = strlen(buf);  
  
        //第四步:写入数据到文件中  
        write(fd,buf,len);  
  
        //第五步:关闭文件  
        close(fd);  
    }  
    return 0;  
}

  • 标准IO 和系统IO的区别
    • 系统IO又称文件IO,也称低级磁盘IO,属于不带缓存的IO(unbuffered I/O)。也就是一般所说的低级I/O——操作系统提供的基本IO服务,与OS绑定,特定于Linux或unix平台。
    • 标准IO被称为高级磁盘IO,标准IO提供三种类型的缓存:
      • u 全缓存:当填满标准IO缓存后才进行实际的IO操作
      • u 行缓存:当输入或输出中遇到新行符时,标准IO库执行IO操作
      • u 不带缓存:例如stderr
    • 设置缓冲类型函数:int setvbuf(FILE *stream,char *buf,int type,unsignedsize)
      • 参数:
        • stream:指向流的指针
        • buf:期待缓冲区的地址
        • type:期待缓冲区的类型
          • _IOFBF(满缓冲):当缓冲区为空时,从流读入数据。或者当缓冲区满时,向流写入数据
          • _IOLBF(行缓冲):每次从流中读入一行数据或向流中写入一行数据
          • _IONBF(无缓冲):直接从流中读入数据或直接向流中写入数据,而没有缓冲区
        • 缓冲区内字节的数量
    • 具体区别:个带缓冲一个不带, 一个是直接找内核走IO,另外一个是先自己准备数据,然后再找内容一次性写。

总结

今天学习了结构体和内核IO,包括标准IO和系统IO。

  1. 结构体,类似数组,只是里面存放的元素不是同一类型的元素,在C 中解决了部分数据封装的问题。
  2. 系统IO又称文件IO,也称低级磁盘IO,属于不带缓存的IO(unbuffered I/O)。也就是一般所说的低级I/O——操作系统提供的基本IO服务,与OS绑定,特定于Linux或unix平台。
  3. 标准IO被称为高级磁盘IO,标准IO提供三种类型的缓存 _IOFBF(满缓冲)、_IOLBF(行缓冲)、_IONBF(无缓冲)。