系统调用open、close、write、read无疑是最基本、最重要、而且也是最复杂的文件操作。除此以外,还有许多用于文件操作或与文件操作有关的系统调用。尽管这些系统调用相比之下只是辅助性的,但是在不同的应用中分别起着很重要的作用,限于篇幅,我们不可能对所有这些系统调用都一一列举并加以介绍。我们可以在后面讲解的系统调用函数跳转表中找到与所有这些系统调用对应的内核函数,对于这些(未必是全部)系统调用的作用于运用可以参考关于Unix、linux程序设计的专著。至于实现这些系统调用的代码,则大多数后面自己要下工作去阅读了。我们在这里选择几个系统调用进行介绍。
先看lseek,这个系统调用的工作和实现虽然简单但却很重要。内核中实现这个系统调用的函数是sys_lseek,其代码如下:
asmlinkage off_t sys_lseek(unsigned int fd, off_t offset, unsigned int origin)
{
off_t retval;
struct file * file;
retval = -EBADF;
file = fget(fd);
if (!file)
goto bad;
retval = -EINVAL;
if (origin <= 2) {
loff_t res = llseek(file, offset, origin);
retval = res;
if (res != (loff_t)retval)
retval = -EOVERFLOW; /* LFS: should only happen on 32 bit platforms */
}
fput(file);
bad:
return retval;
}
参数origin的值只能是0、1或2,表示offset的起点,读者应该是知道的。这段代码中的第76行和77行可能会给我们带来一些困惑,既然把res的值赋给retval,怎么二者又可能不相等呢?这是因为二者的类型不同。变量res的类型是loff_t,实际上是64位整数,而retval的类型为off_t,是32位整数。有关的定义如下:
typedef __kernel_off_t off_t;
typedef __kernel_loff_t loff_t;
数据类型__kernel_off_t和__kernel_loff_t的定义如下:
typedef long long __kernel_loff_t;
typedef long __kernel_off_t;
将64位的数值赋给32位的变量,然后检查二者是否相等,实际上就是检查这个32位数值是否溢出。所以,系统调用lseek在32位系统结构中只适用于文件大小不超过4GB的文件系统。为了突破这个限制,linux另外提供了一个系统调用llseek,使得在32系统结构中也可以处理大于4GB的文件,其代码就在同一个文件中,我们就不看了。不过,二者的主体都是llseek。函数llseek的代码如下:
sys_lseek=>llseek
static inline loff_t llseek(struct file *file, loff_t offset, int origin)
{
loff_t (*fn)(struct file *, loff_t, int);
loff_t retval;
fn = default_llseek;
if (file->f_op && file->f_op->llseek)
fn = file->f_op->llseek;
lock_kernel();
retval = fn(file, offset, origin);
unlock_kernel();
return retval;
}
注意,这个函数返回值的类型是64位的loff_t,参数offset的类型也是loff_t。
具体的文件系统可以通过file_operations结构中的函数指针llseek提供相应的函数,以实现这个操作,如果不提供就等于默认采用default_llseek。就ext2文件系统而言,它所提供的函数为ext2_file_lseek,其代码如下:
sys_lseek=>llseek=>ext2_file_lseek
/*
* Make sure the offset never goes beyond the 32-bit mark..
*/
static loff_t ext2_file_lseek(
struct file *file,
loff_t offset,
int origin)
{
struct inode *inode = file->f_dentry->d_inode;
switch (origin) {
case 2:
offset += inode->i_size;
break;
case 1:
offset += file->f_pos;
}
if (offset<0)
return -EINVAL;
if (((unsigned long long) offset >> 32) != 0) {
if (offset > ext2_max_sizes[EXT2_BLOCK_SIZE_BITS(inode->i_sb)])
return -EINVAL;
}
if (offset != file->f_pos) {
file->f_pos = offset;
file->f_reada = 0;
file->f_version = ++event;
}
return offset;
}
作为参数传递下来的offset可以是负值,但是根据起始点origin加以换算以后就不容许负值了。代码中的第58、59行也是对offset取值范围的检查。位移offset可以超过文件的当前打下,但是不能超过文件大小的上限。这个上限来自两个方面,其一就是不能超过32位整数的大小,这就是第58行所检查的目标;其二是不能超过前一节所讲ext2文件系统中记录块映射机制的总容量。数组ext2_max_sizes及有关的定义如下:
#define EXT2_MAX_SIZE(bits) \
(((EXT2_NDIR_BLOCKS + (1LL << (bits - 2)) + \
(1LL << (bits - 2)) * (1LL << (bits - 2)) + \
(1LL << (bits - 2)) * (1LL << (bits - 2)) * (1LL << (bits - 2))) * \
(1LL << bits)) - 1)
static long long ext2_max_sizes[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
EXT2_MAX_SIZE(10), EXT2_MAX_SIZE(11), EXT2_MAX_SIZE(12), EXT2_MAX_SIZE(13)
};
ext2记录块映射机制的总容量取决于记录块的大小。当记录块大小为1K字节,即2^10时,这个容量的大小为EXT2_MAX_SIZE(10),由直接、间址、二重间址和三重间址四部分相加而得。
除将file结构中的f_pos设置成offset的值以外,还将这结构中的f_reada设置成0,因为既然当前位置变了,原来的预读上下文就废弃了,到下一次读文件时自会建立起新的预读上下文。此外,event是系统中的一个全局变量,file结构中的f_version字段就以event的当前值作为文件读写上下文的版本号。
如果孤立的看ext2_file_lseek的代码,那么似乎就这么一些了。可是,如果把它和写文件操作代码结合起来看,里面还隐藏着lseek的一个重要性质,那就是可以在文件中创造出空洞。原因在于对offset的检验只限于文件大小的上限,而并不受文件当前大小的限制。举例来说,假设文件的当前大小为1KB,而通过lseek把当前位置移到9KB的位置上,然后往文件中写一个字节,这么一来,文件的大小就变成了9KB+1字节,而其中的8KB字节实际上并没有物理记录块,因此成了空洞。空洞的逻辑记录块要往里写的时候才会填上,也就是为之分配物理记录块,在此之前若从空洞中读则全为0。有些应用软件的程序设计利用了lseek的这个性质。不过应该指出,应用软件不能用这个方法来圈地,因为空洞是没有物理记录块的,并且不保证当往空洞中写时一定能分配到物理记录块,能否分配到记录块要视当时设备上是否有空闲记录块而定。
在本博客中要看的第二个系统调用时dup,用来复制一个打开文件号,使新的打开文件号也代表原已存在的文件操作上下文。这个系统调用虽然简单,但却在Unix、linux系统的运行中扮演着很重要的角色。在内核里面,这个系统调用是由sys_dup实现的,其代码如下:
asmlinkage long sys_dup(unsigned int fildes)
{
int ret = -EBADF;
struct file * file = fget(fildes);
if (file)
ret = dupfd(file, 0);
return ret;
}
sys_dup=>dupfd
static int dupfd(struct file *file, int start)
{
struct files_struct * files = current->files;
int ret;
ret = locate_fd(files, file, start);
if (ret < 0)
goto out_putf;
allocate_fd(files, file, ret);
return ret;
out_putf:
write_unlock(&files->file_lock);
fput(file);
return ret;
}
系统调用dup的目的是当前进程的files_struct结构内的数组中将一个已打开文件的file结构指针复制到另一个原来空闲的位置上,使这个新的已打开文件也指向同一个file结构。这里的参数start表示从数组中的什么位置(下标)开始寻找空闲的数组元素。从sys_dup的代码中可以看到传下来的实际参数值是0。所以inline函数locate_fd从打开文件号0所对应的元素开始寻找,必要时还可以扩充数组的容量。这个函数的代码我们在前面已经看到过,这里就不重复了。找到了空闲的打开文件号以后,就通过allocate_fd将作为参数传下来的file结构指针安装在这个打开文件号所对应的位置上,其代码如下:
sys_dup=>dupfd=>allocate_fd
static inline void allocate_fd(struct files_struct *files,
struct file *file, int fd)
{
FD_SET(fd, files->open_fds);
FD_CLR(fd, files->close_on_exec);
write_unlock(&files->file_lock);
fd_install(fd, file);
}
至于fd_install的代码,我们已经在sys_open中阅读过了。操作完成以后,这个新的打开文件号也就代表着原来由sys_dup中的fildes所代表的那个文件了。
看似简单的这么一个操作,却起着十分重要的作用,Unix、linux各种shell的重定向机制就是建立在这个系统调用的基础上。我们通过实例来看看这具体是怎样实现的。先看这么一条shell命令:“echo what is dup?”。这条命令要求shell进程执行一个可执行文件echo,参数为“ what is dup?”。接收到这条命令,shell进程先找到可执行文件bin/echo,然后fork出一个子进程让它执行bin/echo,并将参数传递给它,这个子进程从shell继承了标准输入、标准输出以及标准出错信息三个通道,即打开文件号0、1、2的三个已打开文件,至于这个可执行文件本身所规定的操作则是很简单的,就是把要求在执行时将输出“重定向”到一个磁盘文件foo中去。此时,shell进程大体上将执行以下的操作序列,我们假定在此之前shell进程只有三个已打开文件,即打开文件号0、1、2的三个标准输入、输出文件:
- 打开或创建磁盘文件foo,并截去foo中原有的内容,其打开文件号为3。
- 通过dup复制已打开文件stdout,也就是将打开文件号为1处的file结构指针复制到打开文件号4处,目的是将stdout的file指针暂时保存一下。
- 关闭stdout,即1号已打开文件,由于它的file结构指针已经被复制到打开文件号为4处,这个文件(显示屏)实际上并没有最终地关闭,只是把stdout的位置腾了出来。
- 通过dup,复制3号已打开文件,由于已打开文件1号的位置已经空闲,所以stdout位置上的file结构指针也就指向了磁盘文件foo。
- 通过系统调用fork和exec创建子进程并让子进程执行echo,子进程在执行echo前夕将已打开文件3号和4号关闭而只剩下0、1、2三个已打开文件,但是,此时的stdout,即1号已打开文件实际上已指向磁盘文件foo而不是显示屏,所以当echo将输出往stdout写时就写进了文件foo。
- 至于shell进程本身,则关闭指向foo的1号和3号已打开文件,并且通过系统调用dup和close将原来指向显示屏的file结构指针恢复到stdout位置上,这样shell进程就恢复了开始时的三个标准已打开文件。
由此可见,可执行程序echo其实并不知道它的标准输出文件stdout实际上通向何方,进程和实际输出文件(设备)的结合是运行时由其父进程包办的。
对于stdin和stderr也是同样。这样就简化了对echo的程序设计。因为在程序设计时只要跟是哪个逻辑上存在的文件打交道就行了。熟悉面向对象程序设计大概会联想到“多态”和重载这些概念,从而觉得这似乎也没什么特别高明之处,但是请注意,这个机制的设计与实现是在几十年前。
在dup基础上,后面有衍生一个系统调用dup2,意为“dup to”,不同之处在于这个系统调用多一个参数,即作为目标的打开文件号,也就是说,将一个已打开文件复制到指定的位置上。
再来看系统调用ioctl,这个系统调用通过一个参数来间接地给出具体的操作命令,所以可以认为是对常规系统调用界面的扩充。其作用就好像是补遗或其他,但是操作比较细小,不适合为之专门设置一个系统调用或用去一个系统调用号的,就可以纳入这个系统调用。不仅如此,在开发基于linux内核的应用而需要对内核加以扩充(通常是对特殊设备的驱动)时,也常常通过增设新的ioctl操作命令的办法来实现。特别是当这些扩充不能很自然地落入打开、关闭、读、写这些标准的文件操作界面时。ioctl更是扮演着重要的角色。内核中实现ioctl的是sys_ioctl,其代码如下:
asmlinkage long sys_ioctl(unsigned int fd, unsigned int cmd, unsigned long arg)
{
struct file * filp;
unsigned int flag;
int on, error = -EBADF;
filp = fget(fd);
if (!filp)
goto out;
error = 0;
lock_kernel();
switch (cmd) {
case FIOCLEX:
set_close_on_exec(fd, 1);
break;
case FIONCLEX:
set_close_on_exec(fd, 0);
break;
case FIONBIO:
if ((error = get_user(on, (int *)arg)) != 0)
break;
flag = O_NONBLOCK;
#ifdef __sparc__
/* SunOS compatibility item. */
if(O_NONBLOCK != O_NDELAY)
flag |= O_NDELAY;
#endif
if (on)
filp->f_flags |= flag;
else
filp->f_flags &= ~flag;
break;
case FIOASYNC:
if ((error = get_user(on, (int *)arg)) != 0)
break;
flag = on ? FASYNC : 0;
/* Did FASYNC state change ? */
if ((flag ^ filp->f_flags) & FASYNC) {
if (filp->f_op && filp->f_op->fasync)
error = filp->f_op->fasync(fd, filp, on);
else error = -ENOTTY;
}
if (error != 0)
break;
if (on)
filp->f_flags |= FASYNC;
else
filp->f_flags &= ~FASYNC;
break;
default:
error = -ENOTTY;
if (S_ISREG(filp->f_dentry->d_inode->i_mode))
error = file_ioctl(filp, cmd, arg);
else if (filp->f_op && filp->f_op->ioctl)
error = filp->f_op->ioctl(filp->f_dentry->d_inode, filp, cmd, arg);
}
unlock_kernel();
fput(filp);
out:
return error;
}
参数fd为目标文件的打开文件号,cmd则为具体的操作命令代码。另一个参数arg可以用作对具体操作命令的参数,初看之下似乎只能传递一个整形参数对于很多操作是不够的的,因而限制了ioctl对内核文件、设备操作的扩充能力。其实不然,在需要使用更多参数时可以把这些参数封装在一个数据结构中,然后把arg用作指向该数据结构的指针,所以,实际上传递参数的能力几乎是不受限制的。
在include/linux/ioctl.h中定义了一些命令代码。这些代码的数值大致上是从0x5401-0x545F,也就是说只使用了两个低字节,并且其中较高的字节都是0x54,正好是字符"T"的ASCII代码。由于参数cmd的类型是32位无符号整数,其扩充空间还是相当大的。
但是,从另一个角度看,由于不同的人在不同的应用中都有可能要通过ioctl扩充内核,如何保证命令代码的唯一性而同时又遵循一种统一的格式就成为一个问题,为了这个目的,GNU建议将32位的命令代码cmd划分成4个位段:
对这些位段的定义于操作,如下:
/* ioctl command encoding: 32 bits total, command in lower 16 bits,
* size of the parameter structure in the lower 14 bits of the
* upper 16 bits.
* Encoding the size of the parameter structure in the ioctl request
* is useful for catching programs compiled with old versions
* and to avoid overwriting user space outside the user buffer area.
* The highest 2 bits are reserved for indicating the ``access mode''.
* NOTE: This limits the max parameter size to 16kB -1 !
*/
/*
* The following is for compatibility across the various Linux
* platforms. The i386 ioctl numbering scheme doesn't really enforce
* a type field. De facto, however, the top 8 bits of the lower 16
* bits are indeed used as a type field, so we might just as well make
* this explicit here. Please be sure to use the decoding macros
* below from now on.
*/
#define _IOC_NRBITS 8
#define _IOC_TYPEBITS 8
#define _IOC_SIZEBITS 14
#define _IOC_DIRBITS 2
#define _IOC_NRMASK ((1 << _IOC_NRBITS)-1)
#define _IOC_TYPEMASK ((1 << _IOC_TYPEBITS)-1)
#define _IOC_SIZEMASK ((1 << _IOC_SIZEBITS)-1)
#define _IOC_DIRMASK ((1 << _IOC_DIRBITS)-1)
#define _IOC_NRSHIFT 0
#define _IOC_TYPESHIFT (_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_SIZESHIFT (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_DIRSHIFT (_IOC_SIZESHIFT+_IOC_SIZEBITS)
/*
* Direction bits.
*/
#define _IOC_NONE 0U
#define _IOC_WRITE 1U
#define _IOC_READ 2U
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
/* used to create numbers */
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
例如。linux内核中有对网上电话的支持,在网上电话的驱动程序中需要有个控制收听音量的手段。显然,常规的文件操作例如read、write、lseek等都不适用与这个目的,所以就通过扩充ioctl的方法来实现这项控制,为此目的需要定义一个命令代码。这个代码的定义如下:
#define PHONE_PLAY_VOLUME _IOW ('q', 0x94, int)
读者可以根据上面关于_IOW和各个位段的定义得到命令码PHONE_PLAY_VOLUME的数值为0x4004594。要保证命令码的唯一性,就得保证类型位段或者类型加编号位段取值的唯一性。在从gnu下载的linux源代码中有个文件document/ioctl_number.txt,里面一方面有更具体的说明,另一方面还有个清单,说明类型位段的哪一些数值已经在使用,以及对于给定的类型位段数值哪一些编号已经在使用。需要通过扩充ioctl来实现某些设备驱动的我们应该参阅这个文件。
至于sys_ioctl的代码本身,我们应该没有困难。在switch语句内的四种命令码,即FIOCLEX、FIONCLEX、FIONBIO和FIOASYNC,都是与文件系统无关,只是VFS层上的操作。其中FIOCLEX和当前进程的files_struct结构中的位图close_on_exec内与fd相对应的标志位设成1,使得如果当前进程通过系统调用exec执行一个新的可执行程序时就将这个已打开文件自动关闭,而FIONCLEX则与之相反。FIONCLEX把对于给定已打开文件的操作设置成阻塞或不阻塞模式。至于FIOASYNC,则将对此文件的操作设置成同步或异步模式。通常对已打开文件的操作都是同步和阻塞的,关于不阻塞模式和异步模式可参考后面的进程间通信对socket的操作以及设备驱动中的有关内容。
除这几种命令码以外,对常规文件的操作还是由通用的file_ioctl再加一层过滤,而对设备文件或其他文件(如FIFO)的处理则直接由具体的file_operations结构通过函数指针ioctl提供。函数定义如下:
sys_ioctl=>file_ioctl
static int file_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
{
int error;
int block;
struct inode * inode = filp->f_dentry->d_inode;
switch (cmd) {
case FIBMAP:
{
struct address_space *mapping = inode->i_mapping;
int res;
/* do we support this mess? */
if (!mapping->a_ops->bmap)
return -EINVAL;
if (!capable(CAP_SYS_RAWIO))
return -EPERM;
if ((error = get_user(block, (int *) arg)) != 0)
return error;
res = mapping->a_ops->bmap(mapping, block);
return put_user(res, (int *) arg);
}
case FIGETBSZ:
if (inode->i_sb == NULL)
return -EBADF;
return put_user(inode->i_sb->s_blocksize, (int *) arg);
case FIONREAD:
return put_user(inode->i_size - filp->f_pos, (int *) arg);
}
if (filp->f_op && filp->f_op->ioctl)
return filp->f_op->ioctl(inode, filp, cmd, arg);
return -ENOTTY;
}
操作命令FIBMAP返回文件中给定逻辑块号所对应的物理块号;FIGETBSZ返回文件所在设备的记录块大小;FIONREAD则返回文件中从当前读写位置到文件末尾的距离。
除这三种命令以外,就与对设备文件一样,完全取决于具体的文件系统,直接由具体的file_operations结构中的函数指针ioctl提供。我们之前博客讲过,每个具体文件系统都有至少一个file_operations数据结构。但是,反过来,每个file_operations数据结构去并未必都对应着一个不同的文件系统,这里并不是一对一的关系。例如,ext2文件系统就有两个这样的数据结构。一个是ext2_file_operations,另一个是ext2_dir_operations,分别用于常规文件和目录。这样,在实际设备驱动程序或特殊文件时,只要单独为其设置一个file_operations数据结构,并通过其函数指针ioctl提供一个专用的函数,就可以在这个函数中自行定义和实现所需的ioctl操作命令了。
我们在这里要看的下一个系统调用是link,这个系统调用为已经存在的文件增加一个别名。由link所建立的是硬链接,有别于通过symlink建立的符号链接。在内核中,link是由sys_link实现的,代码如下:
/*
* Hardlinks are often used in delicate situations. We avoid
* security-related surprises by not following symlinks on the
* newname. --KAB
*
* We don't follow them on the oldname either to be compatible
* with linux 2.0, and to avoid hard-linking to directories
* and other special files. --ADM
*/
asmlinkage long sys_link(const char * oldname, const char * newname)
{
int error;
char * from;
char * to;
from = getname(oldname);
if(IS_ERR(from))
return PTR_ERR(from);
to = getname(newname);
error = PTR_ERR(to);
if (!IS_ERR(to)) {
struct dentry *new_dentry;
struct nameidata nd, old_nd;
error = 0;
if (path_init(from, LOOKUP_POSITIVE, &old_nd))
error = path_walk(from, &old_nd);
if (error)
goto exit;
if (path_init(to, LOOKUP_PARENT, &nd))
error = path_walk(to, &nd);
if (error)
goto out;
error = -EXDEV;
if (old_nd.mnt != nd.mnt)
goto out_release;
new_dentry = lookup_create(&nd, 0);
error = PTR_ERR(new_dentry);
if (!IS_ERR(new_dentry)) {
error = vfs_link(old_nd.dentry, nd.dentry->d_inode, new_dentry);
dput(new_dentry);
}
up(&nd.dentry->d_inode->i_sem);
out_release:
path_release(&nd);
out:
path_release(&old_nd);
exit:
putname(to);
}
putname(from);
return error;
}
对于已经阅读了本章前面几节的读者,这里只有一个函数,即vfs_link是新的内容,它的代码如下:
sys_link=>vfs_link
int vfs_link(struct dentry *old_dentry, struct inode *dir, struct dentry *new_dentry)
{
struct inode *inode;
int error;
down(&dir->i_zombie);
error = -ENOENT;
inode = old_dentry->d_inode;
if (!inode)
goto exit_lock;
error = may_create(dir, new_dentry);
if (error)
goto exit_lock;
error = -EXDEV;
if (dir->i_dev != inode->i_dev)
goto exit_lock;
/*
* A link to an append-only or immutable file cannot be created.
*/
error = -EPERM;
if (IS_APPEND(inode) || IS_IMMUTABLE(inode))
goto exit_lock;
if (!dir->i_op || !dir->i_op->link)
goto exit_lock;
DQUOT_INIT(dir);
lock_kernel();
error = dir->i_op->link(old_dentry, dir, new_dentry);
unlock_kernel();
exit_lock:
up(&dir->i_zombie);
if (!error)
inode_dir_notify(dir, DN_CREATE);
return error;
}
这些内容又是我们已经熟悉了的,这里要注意的是对于具有IS_APPEND或IS_IMMUTABLE属性的文件不允许为之建立别名。具体的链接操作以及这种链接到底意味着什么,则因具体的文件系统而异。所以由具体的文件系统通过inode_operations数据结构中的函数指针link来提供,对于ext2文件系统,这个函数是ext2_link。代码如下:
sys_link=>vfs_link=>ext2_link
static int ext2_link (struct dentry * old_dentry,
struct inode * dir, struct dentry *dentry)
{
struct inode *inode = old_dentry->d_inode;
int err;
if (S_ISDIR(inode->i_mode))
return -EPERM;
if (inode->i_nlink >= EXT2_LINK_MAX)
return -EMLINK;
err = ext2_add_entry (dir, dentry->d_name.name, dentry->d_name.len,
inode);
if (err)
return err;
inode->i_nlink++;
inode->i_ctime = CURRENT_TIME;
mark_inode_dirty(inode);
atomic_inc(&inode->i_count);
d_instantiate(dentry, inode);
return 0;
}
显然,这个函数在新文件名(别名)所在目录中创建一个目录项,这个目录项与原来存在的目录项都指向同一个索引节点。这里调用的两个函数ext2_add_entry和d_instantiate,我们都已经看到过了。函数d_instantiate执行完毕,inode结构中的i_dentry队列中就多了一个dentry结构,即代表着文件的别名的dentry结构。同时,这个队列中所有dentry结构中的指针d_inode都指向这同一个inode结构。
由此可见,只要把前面的文件系统的博客读懂,再阅读其他与文件操作相关的代码就不难了。当然,这些博客不可能覆盖所有的文件操作或者某一操作的所有细节,许多内容需要我们自己深入阅读。
如果要问,在文件操作方面还有什么重要的系统调用,那么有两个系统调用值得一提,那就是mknod和select。但是,这两个函数主要用于设备驱动,所以我们把它们放在后面的设备驱动博客中。还有一个重要而且有一定难度的系统调用也是文件操作有关的,那就是mmap。这个系统调用将一个已打开文件的内容映射待进程的内存空间,很大程度上是一个存储管理的问题,所以我们会把它放在后面讲解。