Linux文件操作(上)

1.文件系统的物理结构
1)硬盘的物理结构:驱动臂、盘片、主轴、磁头、控制器
2)磁表面存储器的读写原理
硬盘片的表面覆盖着薄薄的磁性涂层,涂层中含有无数微小的磁性颗粒,谓之磁畴。相邻的若干磁畴组成一个磁性存储元,以其剩磁的极性表示二进制数字0和1。为磁头的写线圈中施加脉冲电流,可把一位二进制数组转换为磁性存储元的剩磁极性。利用磁电变换,通过磁头的读线圈,可将磁性存储元的剩磁极性转换为相应的电信号,表示二进制数。
3)磁道和扇区
磁盘旋转,磁头固定,每个磁头都会在盘片表面画出一个圆形轨迹。改变磁头位置,可以形成若干大小不等的同心圆,这些同心圆就叫做磁道(Track)。每张盘片的每个表面上都有成千上万个磁道。一个磁道,以512字节为单位,分成若干个区域,其中的每个区域就叫做一个扇区(Sector)。扇区是文件存储的基本单位。
4)柱面、柱面组、分区和磁盘驱动器
硬盘中,不同盘片相同半径的磁道所组成的圆柱称为柱面(Cylinder)。整个硬盘的柱面数与每张盘片的磁道数相等。
硬盘上的每个字节需要通过以下参数定位:
磁头号:确定哪个盘面    \
柱面号:确定哪个磁道     > 磁盘I/O
扇区号:确定哪个区域     /
偏移量:确定扇区内的位置
若干个连续的柱面构成一个柱面组
若干个连续的柱面组构成一个分区,每个分区都建有独立的文件系统
若干分区构成一个磁盘驱动器

2.文件系统的逻辑结构
磁盘驱动器:| 分区 | 分区 | 分区 |
分区:| 引导块 | 超级块 | 柱面组 | 柱面组 | 柱面组 |
柱面组:
| 引导块 | 柱面组 | i节点映 | 块位图 | i节点表 | 数据块集 |
| 副    本 | 信   息 | 射    表  |            |              |                |
i节点号:431479    ls -i可查看文件i节点号
i节点
    文件元数据
    100 | 200 | 300
根据目录文件中记录的i节点编号检索i节点映射表,获得i节点下标,用该下标查i节点表,获得i节点,i节点中包含了数据块索引表,利用数据块索引从数据块集中读取数据块,即获得文件数据。
直接块:存储文件实际数据内容
间接块:存储下级文件数据块索引表
100
-----
xxx

200
-----
xxx

300
----
400 | 500 | 600

3.文件分类
普通文件(-):可执行程序、文本、图片、音频、视频、网页
目录文件(d):该目录中每个硬链接名和i节点号的对应表
符号链接文件(l):存放目标文件的路径    ln -s 可以建立符号链接
管道文件(p):有名管道,进程间通信
套接字文件(s):进程间通信
块设备文件(b):按块寻址,顺序或随机读写
字符设备文件(c):按字节寻址,只能以字节为单位顺序读写

4.文件的打开与关闭
打开:在系统内核中建立一套数据结构,用于访问文件
进程表项
    ...
    文件描述符表
        |文件描述符标志 | 文件表项指针 | 0
        |文件描述符标志 | 文件表项指针 | 1
        |文件描述符标志 | 文件表项指针 | 2
        ...                                    |             ^
+-------------------------------------+             |
 |                                                   文件描述符
 v
文件表项
    文件状态标志
    文件读写位置
    v节点指针
    ...    |
+-------+
 |
v
v节点
    i节点内容
    ...

关闭:释放打开文件过程中建立的数据结构
#include <fcntl.h>
打开已有的文件或创建新文件
int open(const char* pathname, int flags, mode_t mode);
成功返回文件描述符,失败返回-1。
pathname - 文件路径
flags - 状态标志,可取以下值:
O_RDONLY - 只读  \
O_WRONLY - 只写  > 只选其一
O_RDWR - 读写      /
O_APPEND - 追加
O_CREAT - 创建,不存在即创建,已存在即打开,除非与以下两个标志之一合用,由此标志mode参数才有效。
O_EXCL - 排它,已存在即失败
O_TRUNC - 清空,已存在即清空,同时有O_WRONLY或O_RDWR
O_SYNC - 写同步,在数据被写到磁盘之前写操作不会完成,读操作本来就是同步的,此标志对读操作没有意义
O_ASYNC - 异步,在文件可读写时产生一个SIGIO信号,在对信号的处理过程中读写I/O就绪的文件,只能用于终端设备或网络套接字,而不能用于磁盘文件
O_NONBLOCK - 非阻塞,读操作不会因为无数据可读而阻塞,写操作也不会因为缓冲区满而阻塞,相反会返回失败,并设置特定的errno
mode - 权限模式,三位八进制:0XXX
                                          ______/  |  \_____       
                                         /               |               \                                    
                                拥有者用户   同组用户  其它用户
4: 可读
2: 可写
1: 可执行
所创建文件的实际权限除了跟mode参数有关,还受权限掩码的影响。
mode=0666
umask=0002
权限=mode&~umask=0664
创建新文件
int creat(const char* pathname, mode_t mode);
flags: O_WRONLY | O_CREAT | O_TRUNC
打开已有文件
int open(const char* pathname, int flags);
关闭文件
int close(int fd);
成功返回0,失败返回-1。
fd - 文件描述符

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
    int fd1 = open("open.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd1 == -1) {
        perror("open");
        return -1;
    }
    printf("fd1 = %d\n", fd1);
    int fd2 = open("open.txt", O_RDONLY);
    if (fd2 == -1) {
        perror("open");
        return -1;
    }
    printf("fd2 = %d\n", fd2);
    close(fd2);
    close(fd1);
    return 0;
}

作为文件描述符表项在文件描述符表中的下标,合法的文件描述符一定是大于或等于0的整数。每次产生新的文件描述符表项,系统总是从下标0开始在文件描述符表中寻找最小的未使用项。每关闭一个文件描述符,无论被其索引的文件表项和v节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中为新的文件描述符所占用。

系统内核缺省为每个进程打开三个文件描述符:
#include <unistd.h>
#define STDIN_FILENO    0 // 标准输入,即键盘
#define STDOUT_FILENO 1 // 标准输出,终端屏幕,有缓冲
#define STDERR_FILENO  2 // 标准错误,终端屏幕,无缓冲
               UC      C               C++
标准输入 0         stdin         cin
标准输出 1         stdout      cout
标准错误 2         stderr       cerr
数据类型 int        FILE*       iostream
文件描述符是用户程序和系统内核关于文件的唯一联系方式

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
void redir(void) {
    close(STDIN_FILENO);
    open("i.txt", O_RDONLY);
    close(STDOUT_FILENO);
    creat("o.txt", 0644);
    close(STDERR_FILENO);
    creat("e.txt", 0644);
}
void resume(void) {
    close(STDIN_FILENO);
    stdin = fopen("/dev/tty", "r");
    close(STDOUT_FILENO);
    stdout = fopen("/dev/tty", "w");
    close(STDERR_FILENO);
    stderr = fopen("/dev/tty", "w");
    setbuf(stderr, NULL);
}
void doio(void) {
    int x, y;
    // 读标准输入
    scanf("%d%d", &x, &y);
    // 写标准输出
    printf("%d+%d=%d\n", x, y, x + y);
    fflush(stdout);
    malloc(0xFFFFFFFF);
    // 写标准错误
    perror("malloc");
}
int main(void) {
    redir();
    doio();
    resume();
    doio();
    return 0;
}

./redir 0<i.txt 1>o.txt 2>e.txt     shell的I/O重定向

5.文件的读取和写入
向指定文件写入字节流
ssize_t write(int fd, const void* buf, size_t count);
成功返回实际写入的字节数(0表示未写入),失败返回-1。
fd - 文件描述符
buf - 内存缓冲区
count - 期望写入的字节数

从文件中读取数据
ssize_t read(int fd, void* buf, size_t count);
成功返回实际读取的字节数(0表示已经读到了文件尾),失败返回-1。
fd - 文件描述符
buf - 内存缓冲区
count - 期望读取的字节数

//write.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
    int fd = open("write.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    const char *text = "Hello, World!";
    printf("写入内容:%s\n", text);
    size_t towrite = strlen(text) * sizeof(text[0]);
    ssize_t written = write(fd, text,towrite);
    if (written == -1) {
        perror("write");
        return -1;
    }
    printf("期望写入%lu字节,实际写入%ld字节。\n",towrite, written);
    close(fd);
    return 0;
}

//read.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
    int fd = open("read.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    char text [256];
    size_t toread = sizeof(text) - sizeof(text[0]);
    ssize_t readed = read(fd, text, toread);
    if (readed == -1) {
        perror("read");
        return -1;
    }
    printf("期望读取%lu字节,实际读取%ld字节。\n",toread, readed);
    text[readed / sizeof(text[0])] = '\0';
    printf("读取内容:%s\n", text);
    close(fd);
    return 0;
}

基于系统调用的文件读写本来就是面向二进制字节流的,因此对二进制读写而言,无需做任何额外的工作。如果要求文件中内容必须是可阅读的,那么就必须通过格式化和文本解析处理二进制形式的数据和文本字符串之间的转换。
代码:binary.c、text.c

//binary.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
    int fd = open("binary.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    char name[256] = "张飞";
    if (write(fd, name, sizeof(name)) == -1) {
        perror("write");
        return -1;
    }
    unsigned int age = 38;
    if (write(fd, &age, sizeof(age)) == -1) {
        perror("write");
        return -1;
    }
    double salary = 20000;
    if (write(fd, &salary, sizeof(salary)) == -1) {
        perror("write");
        return -1;
    }
    struct Employee {
        char name[256];
        unsigned int age;
        double salary;
    }   employee = {"赵云", 25, 10000};
    if (write(fd, &employee, sizeof(
        employee)) == -1) {
        perror("write");
        return -1;
    }
    close(fd);
    if ((fd = open("binary.dat",
        O_RDONLY)) == -1) {
        perror("open");
        return -1;
    }
    if (read(fd, name, sizeof(name)) == -1) {
        perror("read");
        return -1;
    }
    printf("姓名:%s\n", name);
    if (read(fd, &age, sizeof(age)) == -1) {
        perror("read");
        return -1;
    }
    printf("年龄:%u\n", age);
    if (read(fd, &salary, sizeof(salary)) == -1) {
        perror("read");
        return -1;
    }
    printf("工资:%g\n", salary);
    if (read(fd, &employee, sizeof(employee)) == -1) {
        perror("read");
        return -1;
    }
    printf("员工:%s, %u, %g\n", employee.name, employee.age, employee.salary);
    close(fd);
    return 0;
}

//text.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
    int fd = open("text.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    char name[256] = "张飞";
    unsigned int age = 38;
    double salary = 20000;
    char buf[1024];
    sprintf(buf, "%s %u %.2lf\n", name, age, salary);
    if (write(fd, buf, strlen(buf) * sizeof(buf[0])) == -1) {
        perror("write");
        return -1;
    }
    struct Employee {
        char name[256];
        unsigned int age;
        double salary;
    }   employee = {"赵云", 25, 10000};
    sprintf(buf, "%s %u %.2lf\n", employee.name, employee.age, employee.salary);
    if (write(fd, buf, strlen(buf) * sizeof(buf[0])) == -1) {
        perror("write");
        return -1;
    }
    close(fd);
    if ((fd = open("text.txt",
        O_RDONLY)) == -1) {
        perror("open");
        return -1;
    }
    memset(buf, 0, sizeof(buf));
    if (read(fd, buf, sizeof(buf) - sizeof(buf[0])) == -1) {
        perror("read");
        return -1;
    }
    sscanf(buf, "%s%u%lf%s%u%lf", name, &age, 
	    &salary, employee.name, &employee.age,
        &employee.salary); //文本解析
    printf("姓名:%s\n", name);
    printf("年龄:%u\n", age);
    printf("工资: %g\n", salary);
    printf("员工:%s, %u, %g\n", employee.name, employee.age, employee.salary);
    close(fd);
    return 0;
}

6.顺序与随机读写
ABCdef
^     ^   ^
0      3    6
每个打开的文件都有一个与其相关的文件读写位置保存在文件表项中,用以记录从文件头开始计算的字节偏移。文件读写位置通常是一个非负的整数,用off_t类型表示,在32位系统上被定义为long int,而在64位系统上则被定义为long long int。打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设为0,即文件首字节的位置。每一次读写操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,为下一次读写做好准备。因为文件读写位置是保存在文件表项而不是v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有各自的文件读写位置。
人为调整文件读写位置
off_t lseek(int fd, off_t offset, int whence);
成功返回调整后的文件读写位置,失败返回-1。
fd - 文件描述符
offset - 文件读写位置相对于whence参数的偏移量
whence
SEEK_SET - 从文件开始
SEEK_CUR - 从当前位置开始
SEEK_END - 从文件尾开始
lseek函数仅仅是修改文件表项中的文件读写位置,并不引发实际的I/O操作,速度很快。
lseek(fd, 10, SEEK_SET);
lseek(fd, -10, SEEK_END);
lseek(fd, 0, SEEK_CUR); // 返回当前读写位置
lseek(fd, 0, SEEK_END); // 返回文件总字节数
lseek(fd, -10, SEEK_SET); // 错误
lseek(fd, 10, SEEK_END); // 允许,空洞部分补0
代码:seek.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(void) {
    int fd = open("seek.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    const char* text = "Hello, World!";
    if (write(fd, text, strlen(text) * sizeof(text[0])) == -1) {
        perror("write");
        return -1;
    }
    if (lseek(fd, -6, SEEK_CUR) == -1) {
        perror("lseek");
        return -1;
    }
    off_t pos = lseek(fd, 0, SEEK_CUR);
    if (pos == -1) {
        perror("lseek");
        return -1;
    }
    printf("当前读写位置:%ld\n", pos); // 7
    text = "Linux";
    if (write(fd, text, strlen(text) * sizeof(text[0])) == -1) {
        perror("write");
        return -1;
    }
    // 文件的读写位置可以在文件尾之后
    if (lseek(fd, 8, SEEK_END) == -1) {
        perror("lseek");
        return -1;
    }
    text = "<-这里有个洞洞!";
    if (write(fd, text, strlen(text) * sizeof(text[0])) == -1) {
        perror("write");
        return -1;
    }
    off_t size = lseek(fd, 0, SEEK_END);
    if (size == -1) {
        perror("lseek");
        return -1;
    }
    printf("文件大小:%ld\n", size);
    // 文件的读写位置不能在文件头之前
//  if (lseek(fd, -8, SEEK_SET) == -1) {
    if (lseek(fd, -size-1, SEEK_END) == -1) {
        perror("lseek");
        return -1;
    }
    close(fd);
    return 0;
}

hexdump -C seek.txt    查看文件seek.txt的详细内容及对应的ASCII码

7.标准I/O和系统I/O
             应用程序----------+
                    |                   |
                    v                  |
           标准(库)I/O           |
   fopen/fwrite/fclose        |
                    |                   |
                   v                   |
         系统(库)I/O             |
   open/write/close <------+
标准库通过缓冲区优化,减少系统调用的次数,降低在用户态和内核态之间来回切换的频率,提高运行速度,缩短运行时间。

8.复制文件(表项)
进程表项
    ...
    文件描述符表
        |文件描述符标志 | 文件表项指针 | 0
        |文件描述符标志 | 文件表项指针 | 1
        |文件描述符标志 | 文件表项指针 | 2
int fd1  = open(...); // fd1=3
进程表项
    ...
    文件描述符表
        |文件描述符标志 | 文件表项指针 | 0
        |文件描述符标志 | 文件表项指针 | 1
        |文件描述符标志 | 文件表项指针 | 2
        |文件描述符标志 | 文件表项指针 | 3 -> 文件表项
int dup(int oldfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
int fd2 = dup(fd1); // fd2:=7
进程表项
    ...
    文件描述符表
        |文件描述符标志 | 文件表项指针 | 0
        |文件描述符标志 | 文件表项指针 | 1
        |文件描述符标志 | 文件表项指针 | 2
        |文件描述符标志 | 文件表项指针 | 3->文件表项->v节点
        ...                                                                 ^
        |文件描述符标志 | 文件表项指针 | 7 -------+
fd2(7)和fd1(3)对应同一个文件表项,访问同一个文件。
dup函数将oldfd参数所对应的文件描述符表项复制到文件描述符表第一个空闲项中,同时返回该表项所对应的文件描述符。
close(fd1);
close(fd2);
int dup2(int oldfd, int newfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
newfd - 目标文件描述符
dup2函数在复制oldfd参数所标识的源文件描述符表项时,会首先检查由newfd参数所标识的目标文件描述符表项是否空闲,若空闲则直接将前者复制给后者,否则会先将目标文件描述符newfd关闭,再行复制。
fd1 = open("1.txt", ...); --> 文件表项 \
                                                                > v节点
fd2 = open("1.txt", ...); --> 文件表项 /

fd1 = open("2.txt", ...); \
                                           > 文件表项 -> v节点
fd2 = dup(fd1);              /

9.文件控制
int fcntl(int fd, int cmd, ...);
复制文件(表项)
int fcntl(int oldfd, F_DUPFD, int newfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
newfd - 目标文件描述符
该函数类似dup2函数,但略有不同。如果newfd处于打开状态,该函数并不会象dup2函数那样关闭它,而是另外寻找一个比它大的最小的空闲文件描述符作为复制目标。
获取/设置文件描述符标志
截止目前只有一个文件描述符标志位:FD_CLOEXEC
一个进程可以通过exec函数族启动另一个进程取代其自身。
原进程中没有FD_CLOEXEC标志位的文件描述符在新进程中会依然保持打开状态,这也是文件描述符的默认状态。如果原进程中的某个文件描述符带有此标志位,那么在新进程中该文件描述符会被关闭。

int fcntl(int fd, F_GETFD);// 获取文件描述符标志
成功返回文件描述符标志,失败返回-1。
int fcntl(int fd, F_SETFD, int flags);// 设置文件描述符标志
成功返回0,失败返回-1。

获取/追加文件状态标志
int fcntl(int fd, F_GETFL);// 获取文件状态标志
成功返回文件状态标志,失败返回-1。
与文件创建有关的三个状态标志:O_CREAT/O_EXCL/O_TRUC,无法被获取。
只读标志O_RDONLY的值为0,不能与位与检测。
int flags = fcntl(fd, F_GETFL);
if ((flags & O_ACCMODE) == O_RDONLY)  // 只读文件
if (flags & O_WRONLY)  // 只写文件
if (flags & O_RDWR)  // 读写文件
...
int fcntl(int fd, F_SETFL, flags);// 追加文件状态标志
成功返回0,失败返回-1。
只有O_APPEND和O_NONBLOCK两个状态标志可被追加

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

暗里い着迷

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

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

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

打赏作者

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

抵扣说明:

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

余额充值