创建文件早期的UNIX中使用creat()来创建一个文件。这就带来一个问题竞态条件。进程A创建一个文件,可以使用如下伪代码:
if (!FileExists(pathName))
creat(pathName, ...);
问题就处在这里——判断文件是否存在与创建该文件不是一个原子操作,如果另一个进程B也执行这段代码,在某个进程调用creat()睡眠时,另一个调用creat(),这样必然会有一个失败的调用。后来的UNIX修正了这个问题,同时也使open()调用可以用来创建一个文件,这样就避免了该竞态条件。——但对于用户程序来说,竞态条件依然存在,描述creat()的伪代码时将看到。
creat()的原型如下:int creat(char * pathName, int mode);下面是实现其的伪代码:
FileDescriptor creat(string pathName, Mode mode)
{
INode * pINode = namei(pathName);
if (pINode)
{
if (AccessDenied())
{
iput(pINode);
return error;
}
}
else
{
INode * panINode = ialloc();
在父目录中写入数据:新文件名及该文件对应的inode号;
}
FileObj * anObj = AllocFileObj();
初始化anObj;
if (pInode)
{
// 此处将导致用户数据的不一致性,但内核数据结构的一致性得到保证。
释放文件的所有数据块(清空文件内容);
}
Unlock(panINode);
return IndexOf(anObj);
}
当使用namei()查找文件的inode时,内核会在u area中记录其父目录的inode,并锁住它。另外,在该目录中还要找到一个空的目录项以便将文件名及inode号写入其中。因此,该空目录项的偏移量也被记录到u area中。在创建文件之前,还要检查进程对该目录是否有写权。
创建文件之后,需要将新文件的信息写入父目录的inode,然后释放父目录的inode,因为在namei()中被锁住。然后将新文件的inode写到磁盘上以便后续的访问能够立刻生效,最后将父目录的inode更新到磁盘上。这里采用这样的顺序是为了避免在两次写磁盘操作时系统崩溃导致的问题。如果顺序相反,那么有可能父目录中含有新文件信息,但磁盘上却没有,访问就会失败。
在Linux 0.99.15中,creat()实际上是对open()的调用:
asmlinkage int sys_creat(const char * pathname, int mode)
{
return sys_open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);
}
创建特殊文件在UNIX系统中,命名管道、设备文件、目录都是特殊文件。要创建这些文件,用的就是另外一个系统调用mknod()。
void mknod(string pathName, Mode mode, Permission perm, Device dev)
{
if (!IsNamedPipe(mode) && !IsSuperUser())
{
return error;
}
INode * pParentDir = namei(pathName + "/..");
if (pathName已经存在)
{
iput(pParentDir);
return error;
}
为新节点分配inode;
在父目录中建立新目录表项,写入文件名和inode号;
iput(pParentDir);
if (IsBlock(mode) || IsChar(mode))
{
将主次设备号写入新inode;
}
释放新inode;
}
在Linux中,该操作也是与具体文件系统类型相关的,通用的inode操作接口调用这些文件系统的mknod()函数。
改变目录及root目录系统启动时,进程0将文件系统的根目录作为其当前目录,获得根目录的inode后将其保存在u area中作为当前目录。当其调用fork()创建子进程时,子进程就自动拷贝了u area中的inode,作为当前目录。由于进程有一个工作目录,因此,需要将目录改变为工作目录。
void chdir(string pathName)
{
INode * pINode = namei(pathName);
if (!IsDIR(pINode) || !NoPermission())
{
iput(pInode);
return error;
}
pInode->refCount++;
iput(u_area->inode);
u_area->inode = pInode;
}
此后,该进程将该inode作为查找起点,来查找相对路径名所对应的inode。该inode的引用计数至少为1。u area中一直保存该inode,直到下一个chdir或者进程退出。
Linux 0.99.15中的实现:
asmlinkage int sys_chdir(const char * filename)
{
struct inode * inode;
int error;
error = namei(filename,&inode);
if (error)
return error;
if (!S_ISDIR(inode->i_mode)) {
iput(inode);
return -ENOTDIR;
}
if (!permission(inode,MAY_EXEC)) {
iput(inode);
return -EACCES;
}
iput(current->pwd);
current->pwd = inode;
return (0);
}
当进程使用绝对路径名来查找inode时,就需要根目录“/”的inode来作为起点。内核中保存了一个全局变量,用来存放该inode。当用户需要模仿一个文件系统并在那里运行,就可以使用chroot()来改变根目录。chroot()与chdir()类似,但不会释放老的根目录的inode。当chroot()完成后,用户仍然在老的根目录下,但执行cd ..将转到新的根目录下。
管道管道是UNIX中一个有名的机制,对于管道,UNIX用户和程序员褒贬不一。但无论如何,我们还是要了解它。
管道式一个先进先出的结构,利用它可以同步进程,也可以在进程间通信。有两种管道:命名管道和匿名管道。创建命名管道使用open(),而匿名管道使用的是pipe()。另外,命名管道可以在无关进程之间使用,而匿名管道只能使用与相关进程之间。然而在读写数据时,两种管道并无太大区别,都是用read()、write()来读写。关闭这两种管道也是使用同一个系统调用close()。
创建匿名管道的系统调用是pipe(),如下所示:
void pipe(FileDescriptor fd[]) // 即void pipe(FileDescriptor * fd)
{
从管道设备中分配一个inode;
分配两个FileObj,一个用于读,一个用于写;
初始化这两个FileObj,使它们指向刚分配inode;
fd[0] = FileObj(读)的索引;
fd[1] = FileObj(写)的索引;
将该inode的引用计数置为2;
将该inode的读、写计数分别置1;
}
另外,在inode记录下次读或者写的起始位置。而且不能使用lseek()来进行管道读写位置定位,因此管道只能按照特定的顺序操作。
命名管道与匿名管道的语义一样,只不过前者有目录项且可以通过路径名来存取,因此其在文件系统中是永久存在的。打开命名管道与普通文件相同,只不过在完成open()之前,增加相应的inode的读/写计数。这样,如果读/写计数不相等,则打开的进程睡眠直到读/写计数匹配——别的进程为读/写打开该管道。也可以使用nonblock标志来修改这种行为。
在数据存储上,管道只是用inode中的direct block来存放数据,并将这些块组织成循环队列。与普通文件不同的是,管道将读写偏移量存储在inode中。
管道的关闭与普通文件的关闭一样,不过内核在释放管道的inode时需要确定没有进程打开该管道。当关闭管道写端时有进程等待读取数据,这些进程将返回,因为没有进程再往管道里写数据了。而当读端全部关闭后,写端进程将会被唤醒被指示有错误发生。
复制文件描述符有些时候,进程需要将输入或者输出重定向到一个已知的fd,这是就需要复制fd。dup()和dup2()就是用来完成这项工作的,并且通过dup()可以达到共享文件偏移量的目的。dup()将一个文件描述符拷贝到进程文件描述符表的第一个空槽中。如下:
FileDescriptor dup(FileDescriptor fd)
{
FileObj * pFileObj = FileObjTable[fd];
for (FileDescriptor temp = FileObjTable.begin(); temp != FileObjTable.end(); ++temp)
{
if (!FileObjTable[temp])
{
FileObjTable[temp] = pFileObj;
pFileObj->refCount++;
break;
}
}
return temp;
}
Linux 0.99.15的实现如下:
static int dupfd(unsigned int fd, unsigned int arg)
{
if (fd >= NR_OPEN || !current->filp[fd])
return -EBADF;
if (arg >= NR_OPEN)
return -EINVAL;
while (arg < NR_OPEN)
if (current->filp[arg])
arg++;
else
break;
if (arg >= NR_OPEN)
return -EMFILE;
FD_CLR(arg, ¤t->close_on_exec);
(current->filp[arg] = current->filp[fd])->f_count++;
return arg;
}
参考:
The Design of The UNIX Operation System, by Maurice J. Bach
Linux Kernel Source Code v0.99.15, by Linus Torvalds
Linux Kernel Source Code v2.6.22, by Linus Torvalds and Linux community.
Understanding The Linux Kernel, 3rd edition, by Daniel P. Bovet, Marco Cesati
Copyleft (C) 2007 raof01. 本文可以用于除商业用途外的所有用途。若要用于商业用途,请与作者联系。