第七章 文件操作
7.1 文件操作级别
文件操作分为五个级别,按照从低到高的顺序排列如下:
- 硬件级别:硬件级别的文件操作包括:
- fdisk:将硬盘、u盘或SDC盘分区。
- mkfs:格式化磁盘分区,为系统做好准备。
- fsck:检查和维修系统。
- 碎片整理:压缩文件系统中的文件。
- 操作系统内核中的文件系统函数:
每个操作系统内核均可为基本文件操作提供支持,下文列出了类unix系统内核中的一些函数,其中前缀k表示内核函数。
函数名 | 功能 |
---|---|
kmount(),kumount() | (mount/umountfilesystems) |
kmkdir(),krmdir() | (make/removedirectory) |
kchdir(),kgetcwd() | (changedirectory,getCWDpathname) |
klink(),kunlink() | (hardlink/unlinkfiles) |
kchmod(),kchown(),kutime() | (changer |
kcreat(),kopen() | (create/openfileforR,W,RW,APPEND) |
kread(),kwrite() | (read/writeopenedfiles) |
klseek(),kclose() | (lseak/closefiledescriptors) |
ksymlink(),kreadlink() | (create/readsymboliclinkfiles) |
kstat(),kfstat(),klstat() | (getfilestatus/information) |
kopendir(),kreaddiz() | (open/readairectories) |
- 系统调用:
用户模式程序使用系统调用来访问内核函数。例如,下面的程序可读取文件的第二个1024字节。
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd, n;
char buf[1024];
if (fd = open(argv[1], O_RDONLY) <0)
exit(1);
int k = lseek(fd, 1024, SEEK_SET);
n = read(fd, buf, 1024);
close(fd);
printf("%d\n", n);
return 0;
}
open(),read(),lseek()和close()都是c语言库函数。每个库函数都会发出一个系统调用,使进程进入内核模式来执行相应的内核函数,例如open可以进入kopen(),read()可进入当进程结束执行内核函数使,会返回到用户模式,并得到所需的结果。
-
I/O库函数:
系统调用可以让用户读/写多个数据块,这些数据块只是一系列字节。他们不知道,也不关心数据的意义。用户通常需要读/写单独的字符、行或数据结构记录等。应用分类 具体函数 FILE mode I/O fopen(),fread();fwrite(),fseek(),fclose(),fflush() char mode I/O getc(), getchar(); ugetc(); putc(),putchar() line mode I/O gets() , fgets();puts( ) , fputs() formatted I/O scanf(),fscanf().sscanf(); printf(),fprintf() , sprintf() -
用户命令:
用户可以使用Unix/Linux命令来执行文件操作,而不是编写程序。用户命令如下:
mkdir,rmdir,cd,pwd,ls,link,unlink,rm,cat,cp,mv,chmod,etc.
- sh脚本:
虽然比系统调用方便的多,但是必须要手动呼入命令,如果使用的是GUI,必须要拖放文件图标和点击指向设备来输入,操作繁琐而且耗时。
sh语言包含所有的有效Unix/Linux命令,也支持变量和控制语句,如if、do、for、while、case
等。
7.2 文件I/O操作
7.2.1 用户模式
(1)用户模式下的程序执行操作:
FILE *p = fopen("file", "r"); or FILE *p = fopen( "file", "w");
可以打开一个读/写文件流。
(2)fopen()在用户(heap)空间中创建一个FILE结构体,包含一个文件描述符fd、一个fbuf[BLKSIZE]和一些控制变量。它会向内核中的kopen()发出一个fd = open("file",flags=READ or WRITE)系统调用,构建一个OpenTable来表示打开文件示例。OpenTable的mptr指向内存中的文件INODE。对于非特殊文件,INODE 的i_block数组指向存储设备上的数据块。成功后,fp会指向FILE结构体,其中fd是open()系统调用返回的文件描述符。
(3) fread(ubuf, size,nitem, fp):将nitem个size字节读取到ubuf上,通过:
- 将数据从FILE结构体的fbuf上复制到ubuf上,若数据足够、则返回。
- 如果fbuf没有更多数据,则执行(4a)。
(4a)发出read(fd, fbuf, BLKSIZE)系统调用,将文件数据块从内核读取到fbuf上,然后将数据复制到ubuf上,直到数据足够或者文件无更多数据可复制。
(4b)fwrite(ubuf, size, nitem, fp):将数据从ubuf复制到 fbuf。
- 若(fbuf有空间):将数据复制到fbuf上,并返回。
- 若(fbuf已满):发出 write(fd, fbuf, BLKSIZE)系统调用,将数据块写入内核,然后再次写入fbuf。
这样,fread()/fwrite()会向内核发出read()/write()系统调用,但仅在必要时发出,而且它们会以块集大小来传输数据,提高效率。同样,其他库I/O函数,如 fgetc/fputc、fgets/fputs、fscanf/fprintf等也可以在用户空间内的FILE结构体中对fbuf进行操作。
7.2.2 内核模式
(1)内核中的文件系统函数:
假设非特殊文件的read(fd, fbuf[], BLKSIZE)系统调用。
(2)在read()系统调用中,fd是一个打开的文件描述符,它是运行进程的fd数组中的一个索引,指向一个表示打开文件的 OpenTable。
(3)OpenTable包含文件的打开模式、一个指向内存中文件 INODE的指针和读/写文件的当前字节偏移量。从OpenTable的偏移量,
- 计算逻辑块编号lbk。
- 通过 INODE.i_block[]数组将逻辑块编号转换为物理块编号blk 。
(4)Minode包含文件的内存INODE。EMODE.i_block[]数组包含指向物理磁盘块的指针。文件系统可使用物理块编号从磁盘块直接读取数据或将数据直接写入磁盘块,但将会导致过多的物理磁盘I/O。
(5)为提高磁盘VO效率,操作系统内核通常会使用一组I/O缓冲区作为高速缓存,以减少物理I/O的数量。
(5a)对于read(fd, buf, BLKSIZE)系统调用,要确定所需的(dev, blk)编号,然后查询I/O缓冲区高速缓存,以执行以下操作:
.get a buffer = (dev, blk);
.if (buffer's data are invalid){
start_io on buffer;
wait for I/O completion;
}
.copy data from buffer to fbuf;
.release buffer to buffer cache;
(5b)对于write(fd, fbuf, BLKSIZE)系统调用,要确定需要的(dev, blk)编号,然后查询IO缓冲区高速缓存,以执行以下操作:
.get a buffer = (dev, blk):
.write data to the I/O buffer;
.mark buffer as dataValid and DIRTY (for delay-write to disk);
.release the buffer to buffer cache;
(10)设备I/O:I/O缓冲区上的物理I/O最终会仔细检查设备驱动程序,设备驱动程序由上半部分的start_io()和下半部分的磁盘中断处理程序组成。
Upper-half of disk driver
start_io(bp): //bp=a locked buffer in dev_list,opcode=R|W(ASYNC)
{
enter bp into dev's I/O_aueue;
if (bp is FIRST in I/O_queue)
issue I/O command to device;
}
Lower-half of disk driver
Device_Interrupt_Handler:
{
bp = dequeue(first buffer from dev.I/O_queue);
if(bp was READ){
mark bp data VALID;
wakeup/unblock waiting process on bp;
}
else // bp was for delay write
release bp into buffer cache;
if(dev.I/O_queue NOT empty)
issue I/O command for first buffer in dev.I/O_queue;
}
7.3 低级别文件操作
7.3.1 分区
一个块存储设备,如硬盘、U盘、SD卡等,可以分为几个逻辑单元,称为分区。各分区均可以格式化为特定的文件系统,也可以安装在不同的操作系统上。大多数引导程序,如GRUB、LILO等,都可以配置为从不同的分区引导不同的操作系统。分区表位于第一个扇区的字节偏移446 (0x1BE)处,该扇区称为设备的主引导记录(MBR)。表有4个条目,每个条目由一个16字节的分区结构体定义,即:
stuct partition {
u8 drive; //0x80 - active
u8 head; //starting head
u8 sector; //starting sector
u8 cylinder: //starting cylinder
u8 sys_type; //partion type
u8 end_head; //end head
u8 end_sector; //end sector
u8 end_cylinder; //end cylinder
u32 start_sector; //starting sector counting from 0
u32 nr_sectors; //number of sectors in partition
};
7.4 EXT2文件系统简介
7.4.1 EXT2文件系统数据结构
在Linux下,我们可以创建一个包含简单EXT2文件系统的虚拟磁盘,如下文所示。
Block#0:引导块 B0是引导块,文件系统不会使用它。它用于容纳从磁盘引导操作系统的引导程序。
7.4.2 超级块
Block#1:超级块(在硬盘分区中字节偏移量为1024)用于容纳关于整个文件系统的信息。下面说明了超级块中一些重要字段。
struct et2_super block {
u32 s_inodes_count; /* Inodes count */
u32 s_blocks_count; /* Blocks count */
u32 s_r_blocks_count; /* Reserved blocks count */
u32 s_free blocks_count; /* Free blocks count */
u32 s_free_inodes_count; /* Free inodes count */
u32 s_first_data_block; /* First Data Block */
u32 s_log block_size; /* Block size */
u32 s_log_cluster_size; /* Al1ocation cluster size */
u32 s_blocks per_group; /* # Blocks per group * /
u32 s_clusters per_group; /* # Fragments per group */
u32 s_inodes_per_group; /* # Inodes per group * /
u32s_mtime; /* Mount time * /
u32s_wtime; /* write time */
u16s_mnt_count; /* Mount coune* /
s16 s_max_ntcount; /* Maximal mount count */
u16 B_magic; /* Magic signature */
//more non-essential fields
u16 s_inode_size; /* size of inode structure*/
}
大多数超级块字段的含义都非常明显。只有少数几个字段需要详细解释。
- s_first_data_block:0表示4KB块大小,1表示1KB块大小。它用于确定块组描述符的起始块,即s_first_data_block +1。
- s_log_block_size确定文件块大小,为1KB*(2**s_log_block_size),例如0表示 1KB块大小,1表示2KB块大小,2表示4KB块大小,等等。最常用的块大小是用于小文件系统的1KB和用于大文件系统的4KB。
- s_mnt_count:已挂载文件系统的次数。当挂载计数达到max_mount_count时,fsck会话将被迫检查文件系统的一致性。
- s_magic是标识文件系统类型的幻数。EXT2/3/4文件系统的幻数是OxEF53。
7.4.3 块组描述符
Block#2:块组描述符(硬盘上的s_first_data_blocks-1)EXT2将磁盘块分成几个组,每个组有8192个块(硬盘上的大小为32K)。每组用一个块组描述符结构体描述。
struct ext2_group_dese {
u32 bg_b1ock_bitmap; //Bmap bloak number
u32 bg_inode_bitmap; //Imap block number
u32 bg_inode_table; //Inodes begin block number
u16 bg_free_blocks_count; //THESE are OBVIOUS
u16 bg_free_inodes_count;
u16 bg_used_dirs_count;
u16 bg_pad; // ignore these
u32 bg_reserved[3];
};
由于一个软盘只有1440个块,B2只包含一个块组描述符。其余的都是0。在有大量块组的硬盘上,块组描述符可以跨越多个块。块组描述符中最重要的字段是bg_block_bitmap.bg_inode_bitmap和 bg_inode_table,它们分别指向块组的块位图、索引节点位图和索引节点起始块。对于Linux格式的EXT2文件系统,保留了块3到块7。所以,bmap=8,imap=9,inode_table= 10。
7.4.4 位图
Block #8 块位图(Bmap)
位图用来表示某种项的位序列,例如磁盘块或索引节点。位图用于分配和回收项。在位图中,0表示对应项处于FREE状态,1表示处于IN_USE状态。1个软盘有1440个块,但Block#0未被文件系统使用,所以对应位图只有1439个有效位,无效位视作IN_USE处理,设置为1。
Block #9 索引节点位图(Imap)
一个索引节点就是用来代表一个文件的数据结构。EXT2文件系统是使用有限数量的索引节点创建的。各索引节点的状态用B9 中 Imap中的一个位表示。在EXT2 FS 中,前10个索引节点是预留的。所以,空EXT2FS的Imap 以10个1开头,然后是0。无效位再次设置为1。
第八章
8.1 系统调用
在操作系统中,进程以两种不同的模式运行,即内核模式和用户模式,简称Kmode和Umode。在Umode中,进程的权限非常有限。它不能执行任何需要特殊权限的操作。特殊权限的操作必须在Kmode下执行。系统调用(简称syscall)是一种允许进程进入Kmode以执行Umode不允许操作的机制。复刻子进程、修改执行映像,甚至是终止等操作都必须在内核中执行。
8.2 系统调用手册页
在Unix以及大多版本的Linux中,在线手册页保存在/usr/man/目录中(Goldt等1995;Kerrisk 2010,2017)。而在Ubuntu Linux中,则保存在/usr/share/man目录中。man2子目录中列出了所有系统调用手册页。sh命令man 2 NAME显示了系统调用名称的手册页。例如:
man 2 stat
man 2 open
man 2 read
许多系统调用需要特别包含头文件,手册页的SYNOPSIS(概要)部分列出来这些文件。如果没有合适的头文件,C编译器可能会因为syscall函数名称类型不匹配而发出许多警告。一些系统调用可能还需要特定的数据结构作为参数,必须在手册页中描述这些参数。
8.3 简单的系统调用
int syscall(int a,int b,int c,int d);
- access:检査对某个文件的权限
int access(char •pathname, int mode);
- chdir:更改目录
int chdir(const char *path);
- chmod:更改某个文件的权限
int chmod(char *path, mode_t mode);
- chown:更改文件所有人
int chown(char *name, int uid, int gid);
- chroot:将(逻辑)根目录更改为路径名
int chroot (char *patiiname);
- getcwd:获取CWD的绝对路径名
char *getcwd(char *buf, int size);
- mkdir:创建目录
int mkdir(char *pathname, mode_t mode);
- rmdir:移除目录(必须为空)
int rmdir (char *pathname);
- link:将新文件名硬链接到旧文件名
int link(char *oldpath, char *newpath);
- unlink:减少文件的链接数;如果链接数达到0,则删除文件
int uniink(char *pathname);
- symlink:为文件创建一个符号链接
int symliak(char *oldpath, char *newpath);
- rename:更改文件名称
int rename(char *oldpath, char *newpath);
- utime:更改文件的访问和修改时间
int utime(char *pathname, struct utimebuf *time)
以下系统调用需要超级用户权限:
- mount:将文件系统添加到挂载点目录上
int mount(char *specialfile, char *mountDir);
- umount:分离挂载的文件系统
int umount(char *dir);
- mknod:创建特殊文件
int mknod(char *path, int mode, int device);
8.4 常用的系统调用
本节,我们将讨论一些最常见的文件操作的系统调用。其中包括:
stat:获取文件状态信息
int stat(char *filename, struct stat *buf);
int fstat(int filedes, struct stat *buf);
int lstat(char *filename, struct stat *buf);
- open:打开一个文件进行读、写、追加
int open(char *file, int flags, int mode);
- close:关闭打开的文件描述符
int close(int fd);
- read:读取打开的文件描述符
int read(int fd, char buf[], int count);
- write:写入打开的文件描述符
int write(int fd, char buf[], int count);
- lseek:重新定位文件描述符的读/写偏移量
int lseek(int fd, int offset, int whence);
- dup:将文件描述符复制到可用的最小描述符编号中
int dup(int oldfd);
- dup2:将oldfd复制到newfd中,如果文件链接数为0,则删除文件
int dup2(int oldfd, int newfd);
- link:将新文件硬链接到旧文件
int link(char *oldPath, char *newPath);
- unlink:取消某个文件的链接;如果文件链接数为0,则删除文件
int unlink(char *pathname);
- symlink:创建一个符号链接
int symlink(char *target, char *newpath);
- readlink:读取符号链接文件的内容
int readlink(char *path, char *buf, int bufsize);
- umask:设置文件创建掩码;文件权限为(mask & ~umask)
int umask(int umask);
实践截图与代码
1 系统调用实现ls程序
#include<stdio.h>
#include<dirent.h>
#include<string.h>
#include <stdio.h>
#include <iostream>
#include <stdlib.h>
using namespace std;
#define FILE_NAME "/home/lcy/桌面/c"
int main(int argc, char **argv)
{
DIR *dir;
struct dirent *ptr;
dir = opendir(FILE_NAME);
if(NULL == dir)
{
cout << "opendir is NULL" << endl;
return -1;
}
while( (ptr = readdir(dir))!=NULL)
{
printf("d_ino:%ld d_off:%ld d_name: %s\n", ptr->d_ino,ptr->d_off,ptr->d_name);
}
closedir(dir);
return 0;
}
调试过程:
运行代码
g++ myls.cpp -o myls
运行结果与ls命令做对比一致通过
2 系统调用实现文件复制
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc,char **argv)
{
int fd1,fd2,count=0;
char bufs[512];
if(argc<2)
{
printf("Usage is error:./mycp argv1 argv2.\n");
return -1;
}
fd1 = open(argv[1],O_RDONLY,0755);
if(fd1<0)
{
printf("open file1 error!\n");
return -1;
}
fd2 = open(argv[2],O_RDWR|O_CREAT,0755);
if(fd2<0)
{
printf("open file2 error!\n");
return -1;
}
while( (count = read(fd1,bufs,100))>0 )
{
write(fd2,bufs,count);
}
close(fd1);
close(fd2);
}
调试过程:
gcc mycp.c
./a.out 1.txt 2.txt
3 系统调用实现显示文件内容
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define BLKSIZE 4096
int main(int argc, char *argv[ ])
{
int fd,i,m,n;
char buf[BLKSIZE], dummy;
fd = 0;
if(argc > 1){
fd = open(argv[1],O_RDONLY);
if (fd < 0) exit(1);
}
while (n = read(fd,buf,BLKSIZE)){
m = write(1,buf,n);
}
}
4 虚拟磁盘映像
4.1虚拟磁盘映像创建
帮助文档说明:
n命令:创建新分区
p命令:打印分区
4.2 挂载分区
man 8 losetup
:显示用于系统管理的elosetup实用工具命令
(1)用dd命令创建一个虚拟磁盘映像abcdisk
(2)在abcdisk上运行fdisk来创建一个分区p1:
(3)使用以下扇区数在abcdisk的分区1创建一个循环设备:
losetup -o $(expr 2048 \* 512) --sizelimit $(expr 65535 \* 512) /dev/loop14 ldisk
(4)格式化/dev/loop14,他是一个EXT2文件系统:
(5)挂载循环设备
(6)访问作为文件系统一部分的挂载设备:
(7)设备使用完毕后,将其卸载
(8)设备使用完毕后,通过以下命令将其断开
问题解决
问题1:卸载挂载设备时显示busy错误,情况如下:
$ umount /mnt
device is busy
解决方式:
(1)运行下面命令看一下哪个用户哪个进程占用着此设备
fuser -m -v /mnt
(2)运行下面命令杀掉占用此设备的进程
fuser -m -v -k /mnt
(3)重新卸载,发现可以运行成功
问题2:man手册不全,使用man read时发现没有,情况如下
$ man 2 read
在第 2 节中没有关于 open 的手册页条目。
解决方式:执行下列命令进行安装
sudo apt-get install manpages-de manpages-de-dev manpages-dev glibc-doc manpages-posix-dev manpages-posix