新手UC学习记录之I/O文件操作

目录

前言

一、文件系统

1.文件系统的逻辑结构

2.文件访问流程

二、文件操作函数

1.标准库函数

2.系统调用函数

3.标准库函数与系统调用函数对比

a.标准库函数写入

b.系统调用函数写入

c.测试两种写入速度​编辑

d.总结

三、原子操作

1.竞争条件

2.原子操作解释

3.文件锁内核结构

4.文件锁函数

5.解决进程并发访问冲突



前言

本章会先讲自己文件的一些理论,在此期间会穿插对函数的使用演示,如有讲的不对或者不准确的地方,请及时指正,谢谢各位。

参考书籍《UNIX环境高级编程》第三版


提示:以下是本篇文章正文内容,下面案例可供参考

一、文件系统

有时候我会想,在我们一次一次打开文件,又一次一次关闭文件的时候,我们的操作系统会做什么,操作二字可已经代表了操作系统不平凡的地位,于是这节我们主要来深究操作系统以及相关函数

1.文件系统的逻辑结构

自举块(引导块):当计算机启动时,BIOS会从自举块中读取执行指令以及文件系统

超级块:记录文件系统整体信息

数据块:分为直接块与间接块

        -直接块:存储着文件数据

        -间接块:记录着下级数据块地址

i节点:存着文件元数据以及数据块编号,数据块编号可以指向数据块

块位图:描述块(如数据块)是否处于空闲状态,用0表示处于空闲,用1表示此块被占用

2.文件访问流程

通过得到的文件名我们可以得到文件的i节点号,通过i节点号我们可以通过i节点图中找到对应的i节点在磁盘的具体位置,根据i节点我们可以利用其中包含的数据块编号找到对应数据块,从而得到文件中所包含数据的具体内容 

二、文件操作函数

在讲述理论之前,我先需要介绍一下在C语言和在UNIX中两种关于文件操作(文件打开、文件写入、文件读取等)的函数,以便后续理解

1.标准库函数

#include <stdio.h>
FILE *fopen(const char *path, const char* mode);
//通过路径打开或者创建文件
/*
  path:文件路径
  mode:打开文件的模式,可取以下值
      - r: 以只读的方式打开文件,文件必须存在
      - r+: 以读写的方式打开文件,文件必须存在
      - w: 以只写的方式打开文件,文件如果存在则覆盖,文件不存在则创建文件
      - w+: 以读写的方式打开文件,文件如果存在则覆盖,文件不存在则创建文件
      - a: 以追加(写)的方式打开文件,文件存在则写入位置在文件尾,文件不存在则创建文件
      - a+: 以读取和追加的方式打开文件,文件存在则读会在开头,追加会在末尾,文件不存在则创建文件
  返回值:成功返回FILE类型的对象,表示打开的文件,失败返回NULL
*/
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
//读取文件内容
/*
    ptr: 指向存放文件数据存储区的指针
    size: 读取单个数据字节的字节大小
    nmemb: 期望读取文件数据的字节数
    stream: 指向FILE对象的指针,这个对象表示我们打开的文件
    返回值: 成功返回实际读取字节数,失败实际读取字节数小于期望读取字节数
*/
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
//写入文件
/*
    ptr: 指向要写入数据的指针
    size: 写入单个数据字节的字节大小
    nmemb: 期望写入文件中数据的数量
    stream: 指向FILE对象的指针,这个对象表示我们打开的文件
    返回值: 成功返回写入字节数,失败返回写入字节数小于实际写入字节数
*/

2.系统调用函数

#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
//打开文件
/*
    pathname: 文件路径,此处写入字符串
    flags: 状态标志,可以取以下值
         - O_RDONLY 只读
		 - O_WRONLY 只写
		 - O_RDWR 读写
		 - O_APPEND 追加
		 - O_CREAT 不存在创建,已存在打开
		 - O_EXCL 不存在创建,已存在报错
		 - O_TRUNC 不存在创建,已存在清空
    mode: 创建文件给予的权限,有特定的值可以赋予权限(如S_IRWXU),不过这统一使用数字来赋予权限
    返回值: 成功返回文件描述符,用来表示打开文件,失败返回-1
*/
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
//读取文件数据
/*
    fd: 文件描述符,表示打开的文件
    buf: 表示存放读取文件数据的存储区
    count: 期望读取字节数
    返回值: 成功返回实际读取字节数,失败返回-1
*/
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
//向文件写入数据
/*
    fd: 文件描述符
    buf: 指向要写入数据的地址
    count: 期望写入字节数
*/

3.标准库函数与系统调用函数对比

既然说了这两种打开文件的函数,那么这会我就有个疑问,这标准库的函数和系统调用函数究竟有什么不同,究竟谁写入速度快一点。我们知道,fopen这一系列的文件函数底层其实都是有调用系统调用函数(open),那是不是说我们系统调用函数要比标准库函数写入速度快呢?我拿代码来比较一下

a.标准库函数写入

//标准库函数 std.c
#include <stdio.h>

int main(void){
    //打开文件
    FILE* fp = fopen("std.txt","w");
    if(fp == NULL)
    {   
        perror("fopen");
        return -1; 
    }   
    //写入数据
    for(int i = 0 ; i < 100000 ; i++)
    {   
        fwrite(&i,sizeof(int),1,fp);
    }   
    //关闭文件
    fclose(fp);
    return 0;
}

b.系统调用函数写入

//系统调用函数 sys.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(void){
    //打开文件
    int fd = open("sys.txt",O_WRONLY | O_CREAT | O_TRUNC);
    if(fd == -1) 
    {   
        perror("oepn");
        return -1; 
    }   
    //写入数据
    for(int i = 0 ; i < 100000 ; i++)
    {   
        write(fd,&i,sizeof(i));
    }   
    //关闭文件
    close(fd);
    return 0;
}

c.测试两种写入速度

d.总结

从上面写入速度的比较我们发现标准库函数是要比系统调用函数要快的,这究竟为什么?这其实牵扯到用户态与内核态的(用户态与内核态的解释在上一篇文章中——新手学习UC记录——mmap和munmap的使用),实际上系统调用函数在这个十万次循环中,它每执行到write函数就会进行一次用户态到内核态的切换,这样来回的切换大大消耗了资源,而标准库函数对此做了一个优化,提供了一块缓冲区,当缓冲区达到特定要求时,才会将数据输出到文件中,这减少了用户态与内核态的切换次数,所以会更快。

三、原子操作

1.竞争条件

当多个进程和线程对一个共享资源进行访问(读操作)或者修改(写操作)时,所造成最终结果错误(读操作)或者文件内容并不是实际所想内容,这种并发访问冲突被称为竞争条件。下面拿代码演示一下竞争条件,代码演示只会演示进程的并发访问冲突,线程会在后续文章讲。

先创建a.txt空白文件,再创建基于所传参数为所写的执行文件

//并发访问冲突
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(int argc,char* argv[]){
    //判断执行文件后是否有参数
    if(argc < 2)
    {   
        fprintf(stderr,"./a.out <str>");
        return -1; 
    }   
    //打开文件
    int fd = open("a.txt",O_WRONLY | O_APPEND);
    if(fd == -1) 
    {   
        perror("open");
        return -1; 
    }   
    //循环向文件写入数据
    for(int i = 0 ; i < 3 ; i++)
    {   
        if(write(fd,argv[1],strlen(argv[1])) == -1) 
        {
            perror("write");
            return -1; 
        }
        usleep(500000);  //模拟写入数据量大
    }   
    //关闭文件
    close(fd);
    return 0;
}

接下来我们再打开一个终端,与这个终端几乎同时执行这个执行文件,看看是否有会产生竞争条件

打开a.txt文件,发现结果并没有实际预期:先是3个aaa后是3个bbb

2.原子操作解释

 为了解决这种冲突于是便有了原子操作。原子操作是一种基于这个一种不会被线程调度机制或其它任务打断的操作。目的是为了解决线程的并发访问冲突问题,以及进程间对同一文件操作导致结果混乱的问题。

3.文件锁内核结构

这篇文章会将与原子操作具有一定关联的函数,也就是文件锁,文件锁是为了解决多个进程对同一文件操作造成结果错误的问题

系统内核会用三种数据结构表示打开的文件

1.每个进程都会有一个记录项,里面包含着一个文件描述符表,文件描述符表中包含着文件描述符以及文件表项指针。在下图中文件描述符表前三个文件描述符为默认占用状态,因为需要标准输入、标准输出以及标准错误

2.内核会为每一个文件都维护一个文件表项,此文件表项是当不同进程打开相同文件时,都会新生成一个文件表项,文件表项包含着文件状态标志、文件读写位置以及v节点指针

3.当打开文件时,会生成一个v节点指针,里面包含着v节点信息(文件类型,以及对此文件各种操作函数指针)、i节点和锁表指针,锁表指针指向的便是文件锁

当两个进程访问同一文件时,先到的进程会为该文件加上文件锁以便可以单独使用,而后到的进程想加文件锁时,系统就会查看该文件的锁表(由链表组成),如果该文件锁表没有内容则可以加锁,但是上一个进程已经加锁了,于是该进程就会处于阻塞或者非阻塞状态

4.文件锁函数

在演示文件锁之前,我们先要直到关于文件锁的函数

#include <fcntl.h>
int fcntl(int fd,F_SETLK/F_SETLKW,struct flock* lock);
//加或者解文件锁
/*
    fd: 文件描述符
    F_SETLK/F_SETLKW: 以非阻塞方式/以阻塞方式加锁
    lock: 一种结构体,里面包含以下内容
        struct flock{
            short l_type;       //锁类型:F_RDLCK(读锁)/F_WRLCK(写锁)/F_UNLCK(解锁)
		    short l_whence;   //锁区偏移起点:SEEK_SET(文件头)/SEEK_CUR(当前读写位置)/SEEK_END(文件末尾)
		    off_t l_start;    // 锁区偏移字节数
		    off_t l_len     // 锁区字节数,0表示一直锁到文件尾
		    pid_t l_pid    // 加锁进程PID,-1表示自动设置
        }
*/

5.解决进程并发访问冲突

//文件锁的操作
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

//加锁函数
int wlock(int fd , int w){  //用w表示是以非阻塞方式加锁还是阻塞方式加锁
    struct flock lock;
    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    lock.l_pid = -1;
    return fcntl(fd , w ? F_SETLK : F_SETLKW , &lock);
}
//解锁函数
int unlock(int fd){
    struct flock lock;
    lock.l_type = F_UNLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    lock.l_pid = -1;
    return fcntl(fd , F_SETLK , &lock);
}

int main(int argc , char* argv[]){
    //判断执行文件后面是否有文件
    if(argc < 2)
    {
        fprintf(stderr,"./a.out <str>\n");
        return -1;
    }
    //打开文件
    int fd = open("lock.txt",O_WRONLY | O_CREAT | O_APPEND , 0664);
    if(fd == -1)
    {
        perror("open");
        return -1;
    }
    //非阻塞加写锁
    while(wlock(fd,1) == -1)
    {
        printf("文件被加锁,哥们等一会\n");
        sleep(1);
    }
    //写入文件
    for(int i = 0 ; i < 3 ; i++)
    {
        if(write(fd,argv[1],strlen(argv[1])) == -1)
        {
            perror("write");
            return -1;
        }
        sleep(2);
    }
    //解锁
    unlock(fd);
    //关闭文件
    close(fd);
    return 0;
}

以上代码我是以非阻塞方式加锁,我们来看看效果如何

我们可以发现,当另一终端在写入文件时,这个终端无法写入文件,直到另一终端写入完成解锁了,这个终端才可以写入

进程间的竞争条件得到解决。

  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值