linux文件IO

系统调用

  • 所谓系统调用是指操作系统提供给用户程序调用的一组“特殊”接口,用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务。
  • 例如用户可以通过进程控制相关的系统调用来创建进程、实现进程调度、进程管理等。
  • 为什么用户程序不能直接访问系统内核提供的服务呢?
    这是由于在Linux中,为了更好地保护内核空间,将程序的运行空间分为内核空间和用户空间(也就是常称的内核态和用户态),它们分别运行在不同的级别上,在逻辑上是相互隔离的。因此,用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间的函数。
  • 但是,在有些情况下,用户空间的进程需要获得一定的系统服务(调用内核空间程序),这时操作系统就必须利用系统提供给用户的“特殊接口”——系统调用规定用户进程进入内核空间的具体位置(方法:软中断+系统调用号)。进行系统调用时,程序运行空间需要从用户空间进入内核空间,处理完后再返回到用户空间。

系统调用、用户空间和内核空间程序关系

1、Linux的虚拟地址空间为0~4G,Linux内核将这4G字节的空间分为用户空间和内核空间两部分。
2、内核空间的虚拟地址范围为0xC0000000到0xFFFFFFFF,该部分空间供内核使用,存放内核代码及内核数据。
3、用户空间虚拟地址范围为 0x00000000到0xBFFFFFFF,供用户的各个进程使用,存放用户代码和用户数据。
4、用户进程无法访问直接访问内核空间,必须通过API触发内核的软中断机制进行系统调用,以达到访问内核空间的目的。

API

  • 前面讲到的系统调用并不是直接与程序员进行交互的,它仅仅是一个通过软中断机制向内核提交请求,以获取内核服务的接口。在实际使用中程序员调用的通常是用户编程接口——API
  • 系统命令相对API更高了一层,它实际上一个可执行程序,它的内部引用了用户编程接口(API)来实现相应的功能。


文件描述符

  • 内核如何区分和引用特定的文件呢?这里用到了一个重要的概念——文件描述符。
  • 对于Linux而言,所有对设备和文件的操作都是使用文件描述符来进行的。
  • 文件描述符是一个非负的整数,它是一个索引值,并指向在内核中每个进程打开文件的记录表。
  • 当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。
  • 通常,一个进程启动时,都会打开3个文件:标准输入、标准输出和标准出错处理。
    这3个文件分别对应文件描述符为0、1和2(也就是宏替换STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO)。

底层文件I/O操作

  • open()函数是用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。


  • close()函数是用于关闭一个被打开的文件。当一个进程终止时,所有被它打开的文件都由内核自动关闭,很多程序都使用这一功能而不显示地关闭一个文件。


  • read()函数是用于将从指定的文件描述符中读出的数据放到缓存区中,并返回实际读入的字节数。若返回0,则表示没有数据可读,即已达到文件尾。读操作从文件的当前指针位置开始。当从终端设备文件中读出数据时,通常一次最多读一行。


  • write()函数是用于向打开的文件写数据,写操作从文件的当前指针位置开始。对磁盘文件进行写操作,若磁盘已满或超出该文件的长度,则write()函数返回失败。


  • lseek()函数是用于在指定的文件描述符中将文件指针定位到相应的位置。它只能用在可定位(可随机访问)文件操作中。管道、套接字和大部分字符设备文件是不可定位的,所以在这些文件的操作中无法使用lseek()调用。


  • 举一个复制文件的例子:

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

#define BUFFER_SIZE 1024
#define SRC_FILE_NAME "srcfile.txt"
#define DEST_FILE_NAME "destfile.txt"
#define OFFSET 1024*10

int main()
{
    int src_fd,dest_fd,real_read_long=0,rtn_num;
    char buffer[BUFFER_SIZE];
    long long int cpnum=0;
    memset(buffer,0,BUFFER_SIZE);
    //open file
    src_fd=open(SRC_FILE_NAME,O_RDONLY);
    dest_fd=open(DEST_FILE_NAME,O_RDWR|O_CREAT,S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
    if(src_fd<0||dest_fd<0)
    {
        printf("open srcfile or destfile fail!\n");
        exit(1);
    }
    //move srcfile data pointer 
    rtn_num=lseek(src_fd,-OFFSET,SEEK_END);
    if(rtn_num==-1)
    {
        printf("move srcfile date pointer fail!\n");
        return 1;
    }
    printf("current srcfile pointer is %d\n",rtn_num);
    //copy srcfile data to destfile
    while(real_read_long=read(src_fd,buffer,BUFFER_SIZE))
    {
        cpnum+=real_read_long;//count copy bytes
        write(dest_fd,buffer,real_read_long);
    }
    printf("Copy finished!(size:%lld bytes)\n",cpnum);
    close(dest_fd);
    close(src_fd);
    
    return 0;
}

文件锁

  • 举例:
    1) 进程“A”打开和读取一个文件,此文件包含账户相关的一些信息。
    2) 进程“B”也打开了这个文件,并读取了文件中的信息。
    3) 现在,进程“A”更改了其副本中的一条余额记录,并将其写入文件。
    4) 此时,进程“B”并不知道上次读取的文件已经被更改,它还保存着原始的文件副本。然后,进程“B”更改了“A”操作的那条相同的记录,并将记录写入文件。
    5) 此时,文件中将只保存了进程“B”更改过的记录。
  • 为避免这种事情发生,就要用文件锁来确保操作的“序列化”。
  • 文件锁包括建议性锁和强制性锁。
    建议性锁又叫协同锁,它要求每个上锁文件的进程都要检查是否有锁存在,并且尊重已有的锁。在一般情况下,内核和系统都不使用建议性锁。
    强制性锁是由内核执行的锁,当一个文件被上锁进行写入操作的时候,内核将阻止其他任何文件对其进行读写操作。采用强制性锁对性能的影响很大,每次读写操作都必须检查是否有锁存在。
  • 在Linux中,实现文件上锁的函数有lockf()和fcntl(),其中lockf()用于对文件施加建议性锁,而fcntl()不仅可以施加建议性锁,还可以施加强制锁。同时,fcntl()还能对文件的某一记录上锁,也就是记录锁。
    记录锁又可分为读取锁和写入锁,其中读取锁又称为共享锁,它能够使多个进程都能在文件的同一部分建立读取锁。而写入锁又称为排斥锁,在任何时刻只能有一个进程在文件的某个部分上建立写入锁。当然,在文件的同一部分不能同时建立读取锁和写入锁。
  • fcntl()函数格式(1)


  • fcntl()函数格式(2)- flock结构
struct flock
{
    short l_type;
    off_t l_start;
    short l_whence;
    off_t l_len;
    pid_t l_pid;
}

文件锁例子:
lock_set.c

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

void lock_set(int fd,short type)
{
    struct flock lock;
    lock.l_type=type;
    lock.l_start=0;
    lock.l_whence=SEEK_SET;
    lock.l_len=0;
    lock.l_pid=-1;
    
    fcntl(fd,F_GETLK,&lock);
    if(lock.l_type!=F_UNLCK)
    {
        if(lock.l_type==F_RDLCK)
            printf("The file %d has already F_RDLCK by process %d\n",fd,lock.l_pid);
        if(lock.l_type==F_WRLCK)
            printf("The file %d has already F_WRLCK by process %d\n",fd,lock.l_pid);
    }
    lock.l_type=type;
    if(fcntl(fd,F_SETLKW,&lock))
        printf("The file %d was faile to be set lock by process %d\n",fd,getpid());
        
    switch(lock.l_type)
    {
        case F_RDLCK:
            printf("The file %d was set readlock by process %d\n",fd,getpid());
            break;
        case F_WRLCK:
            printf("The file %d was set writelock by process %d\n",fd,getpid());
            break;
        case F_UNLCK:
            printf("The file %d was released lock by process %d\n",fd,getpid());
            break;
            
        default:
            break;
    }
}

main.c

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/file.h>
#include<stdio.h>
#include<stdlib.h>
#include"lock_set.h" 

int main()
{
    int fd;
    fd=open("hello",O_RDWR|O_CREAT);

    if(fd<0)
    {
        printf("open file hello fail!\n");
        exit(1);
    }
    lock_set(fd,F_WRLCK);
    write(fd,"Hello,world!",12);
    lock_set(fd,F_UNLCK);
    close(fd);
    return 0;
}

I/O处理的模型

  • 阻塞I/O模型:在这种模型下,若所调用的I/O函数没有完成相关的功能,则会使进程挂起,直到相关数据到达才会返回。如常见对管道设备、终端设备和网络设备进行读写时经常会出现这种情况。
  • 非阻塞模型:在这种模型下,当请求的I/O操作不能完成时,则不让进程睡眠,而且立即返回。非阻塞I/O使用户可以调用不会阻塞的I/O操作,如open()、write()和read()。如果该操作不能完成,则会立即返回出错(例如:打不开文件)或者返回0(例如:在缓冲区中没有数据可以读取或者没空间可以写入数据)。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容