UNIX环境编程(c语言)--文件I/O-文件共享

基础知识

文件描述符

是内核为了高效管理已经打开的文件创建的索引,所有打开的文件都通过文件描述符来引用,其值是一个非负整数,当打开或创建一个文件时,内核会向进程返回一个文件描述符,当读或写时,需要使用文件描述符标识你需要操作的文件。

当程序开始运行时,系统会自动打开三个文件描述符 ,如下显示

文件描述符用途符号常量标准I/O文件流
0标准输入STDIN_FILENOstdin
1标准输出STDOUT_FILENOstdout
2标准出错STDERR_FILENOstderr

当然你可以将标准输入 输出 出错重定向

但我们打开一个文件时,系统返回的文件描述符必定是当前最小可用的文件描述符的值,由于0 1 2 都已经被用了,所以第一次调用返回的肯定是3

文件偏移量

是当前读取位置到文件开头处计算的字节数,通常是一个非负数(有例外),就相当于标记现在的“光标”在文件的哪个位置,通常读、写等操作都是在当前文件偏移量下进行的。我们在操作前应当确定文件偏移量在哪。
普通文件中,文件偏移量必定是非负整数
但是设备文件中,文件偏移量有可能是负数

其他有关于文件名、权限、属性等基础知识
请看,Linux文件操作命令与基本知识 (一)


文件io

打开文件

原型
系统调用 : open 、openat,原型如下

int open(const char *path, int oflag, ... /*mode*/);
int openat(int fd, const char *path, int oflag, ... /*mode*/);
/* 返回值 : 若成功返回文件描述符,若失败返回 -1 */

open 和 openat 的区别在于 后者多了一个参数 fd , 其用法如下

  1. path 是绝对路径,fd将被忽略,open和openat等价
  2. path是相对路径,fd是指定了相对路径的开始地址 (打开的目录返回)
  3. path是相对路径,fd可写 AT_FDCWD,表示使用当前工作目录

参数 path用于指出文件的位置和文件名,只写文件名为当前目录。

参数oflag 是用于指定操作选项,可选择多个选项,用 | (或)连接

必选选项如下,而且下面的只能选一个

选项功能
O_RDONLY只读打开
O_WRONLY只写打开
O_RDWR读写打开
O_EXEC只执行打开

还有很多可选选项,以下列出常用的

选项功能补充
O_APPEND每次写时,都追加在文件尾端append:增补
O_CREAT若文件不存在时创建它creat:创建
O_SYNC每次写时,等待物理操作完成再返回sync:同步
O_TRUNC若文件存在,打开选项中包含写时,将文件长度截为0trunc
O_EXCL若同时指定了O_CREAT,而文件已经存在的话,将报错excl 除外的
O_DIRECTORY若path不是目录,则报错
O_NOCTTY如果是一个终端设备,不分配为该进程的控制终端
O_NONBLOCK如果path是一个FIFO、块设备、字符特殊文件则此选项为文件的本次打开和后续的 I/O操作设置非阻塞模式方式
注:更多选项,可使用目录 man 2 open查看

最后一个参数 …是可变长度的参数,只有创建文件时才有用,用于指定创建文件的权限,用数字设置权限的反思
数字设置权限方法可查看:Linux系统学习—用户管理及权限管理(三)

使用实例

以可读可写方式打开一个文件

int fd = -1;
fd = open("test.txt",O_RDWR);

打开一个文件,若不存在则创建

int fd = -1;
fd = open("test.txt",O_RDWR|O_CREAT,0666);

判断一个文件是否操作,存在返回-1,不存在则创建

int fd = -1;
open("test.txt",O_RDWR|O_CREAT|O_EXCL,0666);

创建文件

原型

int creat(const char *path, mode_t mode);
/* 若成功返回文件描述符,失败返回-1 */

两个参数和open一致,path表示路径和文件名,mode是指明创建权限

creat只能以只写方式打开创建的文件,如果你希望读文件,只能creat后关闭文件,再重新open

所以 在实际应用在比较少用到creat,在早期open还不能创建文件时才经常使用

现在我们经常用以下命令创建文件

open(path, O_RDWR|O_CREAT|O_TRUNC, mode);

修改文件偏移量

原型

off_t lseek(int fd, off_t offset, int whence);
/* 若成功返回新的文件偏移量,若失败返回-1*/

参数一,fd ,是文件标识符,通常fd都表示这个
参数二offset,与参数三whence相关

  • whence 是 SEEK_SET时,文件偏移量设置为,距文件开头offset个字节处
  • whence 是 SEEK_CUR时,文件偏移量设置为当前值+offset,offset可正可负
  • whence 是 SEEK_END时,文件偏移量设置为文件长度+offset,offset可正可负

说明
当打开一个新文件时,除非设置了O_APPEND选项,否则文件偏移量为0

因为文件偏移量有可能是负数,所以测试是否成功,最好测试是否等于-1,而不是测试是否小于0

管道、FIFO、socket不能被设置文件偏移量,lseek时会返回-1

文件偏移量允许大于文件长度,这样子会在文件中形成一个空洞,就是存在一个没有被写过的区域,但是这个区域都会被读为0

使用实例
从文件开头5字节处开始操作

lseek(fd, 5, SEEK_SET);

当前位置后5字节开始写

lseek(fd, 5, SEEK_CUR);

在文件末位前5字节处

lseek(fd, -5, SEEK_END);

读文件

原型

ssize_t read(int fd, void *buf, size_t nbytes);
/* 成功返回读到的字节数,已到文件为返回0,出错返回-1*/

参数二是存放读取数据的内存的地址,一般定义一个char *buf用于存放
参数三,需要读取的字节数

说明
若读取正常,那返回值就是设置的需要读的字节数

但是有如下几种情况

  • 在读到要求字节数之前就到了文件结尾,将返回实际读到的字节数,再读一次将返回0(文件结束)
  • 在终端设备中读取时,一次只能读取一行 (可以改变)
  • 在网络中读取,因为存在缓存,可能比实际的少
  • 在面向记录的设备读取,一次只能读一个记录

使用实例
读取10字节

char buf[];
memset(buf,0,sizeof(buf));  //将buf清空
read(fd, buf, 10);

写文件

原型

ssize_t write(int fd, const void *buf, size_t nbytes);
/* 成功返回已经读到的字节数,出错返回-1*/

参数的定义基本与read一致

但是返回值一般与设置的nbytes一致,否则为出错了
一般出错为磁盘写满了,或者超过了进程的文件长度限制

使用实例
写一个字符串

#define name “guanfuxin”
write(fd, name, sizeof(name));

关闭文件

原型

int close(int fd );
/* 成功返回0,失败返回-1*/

关闭一个文件也会释放加在该文件上的所有记录锁(以后再谈)

当一个进程结束时,内核会自动关闭它打开的文件

但是还是最好自己写好关闭文件的代码

文件io实例

打开test.txt文件,若不存在则创建它,在末尾写入一个字符串,然后读取全部内容

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


#define str "hello world"

int main( int argc, char **argv)
{
    int             fd = -1;
    off_t           offt = -1;
    int             rv = -1;
    char            buf[1024];

    fd =  open("test.txt", O_RDWR|O_CREAT|O_APPEND,0666);    // 打开文件
    if(fd == -1)
    {
        printf(" open error : %s \n", strerror(errno));
        return -1;
    }
    printf("open succed fd[%d] \n " , fd);

    rv = write(fd, str, sizeof(str));     //写入字符串
    if( rv == -1)
    {
        printf(" write error : %s \n", strerror(errno));
        goto clean;
    }

    printf("write succed rv[%d] \n", rv );

    rv = lseek(fd, 0, SEEK_SET);       // 将文件偏移量设置为文件开头
    if(rv == -1)
    {
        printf("lseek error :%s \n", strerror(errno));
        goto clean;
    }
    printf("lseek succed rv[%d] \n ", rv);

    rv = read(fd, buf, sizeof(buf));     // 读取全部内容,假设文件没有超过1024
    if(rv == -1)
    {
        printf("read error : %s \n",strerror(errno));
        goto clean;
    }
    printf("read succed rv[%d],: %s \n", rv , buf);    // 打印读取到的内容

clean:
    close(fd);


    return 0;

}

关于实例內的errno报错
linux中系统调用的出错原因都存储在int errno,这个变量是系统维护的,会存储就近发送的错误,下一次错误会覆盖这一次的错误

但是错误原因是以整数存储在errno中的,对程序员不友好,使用我们使用strerror可以将其转化为字符串形式的错误提醒


文件共享

Unix/linux系统支持在不同进程间共享打开文件
首先我们介绍内核用于所有I/O的数据结构

io的数据结构

内核使用了三种数据结构来表示打开的文件

  1. 每个进程的进程表中都有一个记录项,其中包含了文件描述符表,内有文件描述符和对应指向这个文件的指针

  2. 内核为所有打开的文件维持一张文件表,其中包含了文件状态标志,文件偏移量,指向文件v节点的指针

  3. 每个打开文件都有一个v节点,v节点还包含了i节点

     注:Linux没有采用v节点,而是采用了一个与文件系统相关的i节点和一个与文件系统无关的i节点
    

两个进程打开同一个文件的关系图如下
在这里插入图片描述
画的比较丑,当大致关系如上,Linux中没有v节点,但是实现上也没有差别很大


原子操作

原子操作,就是一个不可分的操作,只调用一个函数调用完成,要么一次完成全部操作,要么全部不执行

假如我们写了一个程序,打开一个文件并在文件尾部写入内容,先使用open打开文件,再使用lseek将文件偏移量设置到文件末尾,然后再写

这样子的 程序在只有一个进程时是没有问题的,但是如果有两个以上的进程同时操作一个文件,就会有意想不到的问题,如下

进程a打开文件test,并将文件偏移量设置到了文件末尾,这时内核将进程a挂起,然后进程b运行,也打开了文件test,并在文件末尾写入了内容,等到进程a再次运行写入内容,这时候它写入的位置就不是在末尾了。

问题就在于,在两个函数调用之间,内核有可能临时将进程挂起

如果使用原子操作,就可以避免这样子的问题

函数pread 和 pwrite

原型

ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
/* 成功返回读到的字节数,已到文件为返回0,出错返回-1*/

ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
/* 成功返回已经读到的字节数,出错返回-1*/

这两个函数和之前的read和write用法差不多
pread 相当于 先lseek 再read
pwrite 相当于 先lseek 再write

参数的定义也是一致的

两个函数作用就是,只用了一个原子操作完成了,设置文件偏移量和 读写的操作

其他文件io函数

复制文件描述符 :函数dup 和dup2

原型

int dup(int fd);
int dup2(int fd, int fd2);
/* 成功返回新的文件描述符,错误返回-1*/

两个函数的作用都是,将新的文件描述符,也指向文件描述符fd指向的文件,两个文件描述符恭喜文件表项(文件状态、文件偏移量一致)

通俗话来说就是,我们打开了文件test,返回的文件描述符为fd,这时我们调用
dup(fd),返回的新文件描述符new_fd 也指向文件test

dup返回的新文件描述符,一定是最小可用文件描述符
dup2可以指定新文件描述符的值为fd2,如果fd2已经打开,会先将其关闭,如果fd= fd2,则直接返回fd,不会关闭一次

常用dup2 来重定向标准输入输出

使用实例

dup2(fd, STDIN_FILENO); //标准输入重定向到fd指向文件中去
dup2(fd, STDOUT_FILENO); //标准输出重定向到fd指向文件中去
dup2(fd, STDERR_FILENO); //标准出错重定向到fd指向文件中去

刷新缓存 函数 sync、fsync、fdatasync

传统的Unix系统实现在内核设有区缓存和页缓存,大多数io操作都通过缓冲区进行,当我们写入文件时,并不会马上写入文件中,而是先写入在缓冲区中,排入队列,再写入磁盘。

为了保证文件内容的一致性,可以使用刷新缓存的函数调用

原型

int fsync(int fd);
int fdatasync(int fd);
/*  成功返回0,失败返回-1*/

void sync(void);

sync是将所有修改过的内容都排入写队列,然后就返回,并不等待写磁盘操作完成。

fsync 只对指定的文件起作用,等待写磁盘操作完成,才返回
fdatasync 类似于fsync,但是只影响数据部分,而fsync还会更新文件的属性

读取 / 修改文件属性 函数 fcntl

可以获取或改变文件的属性
原型

int fcntl(int fd, int cmd, ... /*int arg */);
/* 成功返回与cmd有关,失败返回-1*/

fcntl 的cmd与8种功能,后3种与记录锁有关,暂时不说

cmd取值功能第三个参数
F_DUPFD复制文件描述符,作为函数值返回设置新文件描述符的最小可取值
F_DUPFD_CLOEXEC复制文件描述符
F_GETFD获取文件描述符标志,作为函数值返回
F_SETFD设置文件描述符标志新的标志
F_GETFL将文件标志作为函数值返回(open时设置的)
F_SETFL设置文件标志位第三个参数新文件标志
F_GETOWN获取接收 SIGIO和SIGURG信号的进程id和组id
F_SETOWN设置接收 SIGIO和SIGURG信号的进程id和组id正值为一个进程id,负值代表进程组id(绝对值)

使用实例

获取文件标志时,并不能直接获取全部文件标志。如需获取几个必选的文件标志需要需要& O_ACCMODE

val = fcntl(fd, F_GETFL, 0);
mode = val & O_ACCMODE;   //获取必选的文件标志
if(val & O_APPEND)    //获取可选的文件标志
	printf( " .apend\n");

修改文件标志时,必须先获取之前文件标志,修改后再写入
负责直接写入,会导致之前的标志被刷掉

//添加
val = fcntl(fd, F_GETFL, 0);  
val = val | O_APPEND;
fcntl(fd, F_SETFL, val);
//去除
val = fcntl(fd, F_GETFL, 0);  
val = val & ~(O_APPEND);
fcntl(fd, F_SETFL, val);
  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GuanFuXinCSDN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值