分类: LINUX
2008-11-19 15:34:18
每个运行中的程序被称为进程(process),它有一些与之关联的文件描述符。这是一些小值整数,你可以通过它们访问打开的文件或设备。有多少文件描述符可用取决于系统的配置情况。当开始运行程序时,它一般会有三个已经打开的文件描述符。它们是:
0:标准输入。
l:标准输出。
2:标准错误。
你可以通过系统调用open把其他文件描述符与文件和设备关联起来.我们一会就要介绍到它。同时,使用自动打开的文件描述符,就已经可以让我们通过write来创建一些简单的程序了。
* write 系统调用
系统调用write的作用是,把缓冲区buf的前nbytes个字节写入与文件描述符fildes关联的文件中。它返回实际写入的字节数。如果文件描述符有错或者底层的设备驱动程序对数据块长度比较敏感,该返回值可能会小于nbytes。如果这个函数的返回值是0 ,就表示未写出任何数据,如果是-1,就表示在write调用中出现了错误,对应的错误代码保存在全局变量errno里面。
下面是write系统调用的原型:
/usr/include/unistd.h
extern ssize_t write (int __fd,
__const void *__buf, size_t __n);
# vi simple_write.c
#include
#include
int main()
{
if ((write(1, "Here is some data\n", 18)) != 18)
write(2, "A write error has occurred on file descriptor 1\n",46);
exit(0);
}
运行结果:
# ./simple_write
Here is some data
这个程序只是在标准输出上显示一条消息。当程序退出运行时,所有已经打开的文件描述符都会自动关闭,所以我们不需要明确地关闭它们。但当我们是在处理被缓冲的输出时,情况就不一样了。
需要再次提醒的是,write可能会报告说它写入的字节比你要求的少。这并不一定是个错误。在程序中,你需要检查errno以发现错误,然后再次调用write写入剩余的数据。
* read 系统调用
系统调用read的作用是从与文件描述符fildes相关联的文件里读入nbytes个字节的数据,并把它们放到数据区buf中。它返回实际读入的字节数,它可能会小于请求的字节数。如果read调用返回0,就表示未读入任何数据,已到达了文件尾。同样,如果是-1,就表示read调用出现了错误。
下面是read系统调用的原型:
/usr/include/unistd.h
extern ssize_t read (int __fd,
void *__buf, size_t __nbytes);
# vi simple_read.c
#include
#include
int main()
{
char buffer[128];
int nread;
nread = read(0, buffer, 128);
if (nread == -1)
write(2, "A read error has
occurred\n", 26);
if ((write(1,buffer,nread)) != nread)
write(2, "A write error has
occurred\n",27);
exit(0);
}
运行结果
# ./simple_read
hello
hello
* open 系统调用
创建新的文件描述符需要使用系统调用open
#include
#include
#include
int open(const char *path, int oflags);
int open(const char *path, int oflags, mode_t mode);
严格来说,在符合POSIX 规范的系统上,使用open系统调用并不需要包括头文件sys/types.h和sys/stat.h,但在某些UNIX系统上它们可能是必不可少的。
简单地说,open建立了一条到文件或设备的访问路径。如果操作成功,它将返回一个文件描述符, read和write等系统调用使用该文件描述符就可以对打开的文件进行操作。这个文件描述符是唯一的,它不会与任何其他运行中的进程共享。如果两个程序同时打开同一个文件,会得到两个不同的文件描述符。如果它们都对文件进行写操作,那么它们会各写各的,分别接着上次离开的位置继续往下写。它们的数据不会交织在一起,而是彼此互相覆盖(后写入的内容覆盖掉前面写入的内容)。两个程序对文件的读写位置(偏移值)有各自的理解。为了防止出现这种我们并不希望看到的冲突局面,可以使用“文件锁”功能,我们将在第7章里介绍它。
准备打开的文件或设备的名字作为参数path传递给函数,oflags参数用来定义打开文件所采取的动作。
oflags参数是通过主要文件访问模式与其他可选模式的结合来指定的。open调用必须指定表3-1中所示的文件访问模式之一。
模 式 说 明
O_RDONLY 以只读方式打开
O_WRONLY 以只写方式打开
O_RDWR 以读写方式打开
O_APPEND:把写入数据追加在文件的末尾。
O_TRUNC:把文件长度设置为零,丢弃已有的内容。
O_CREAT:如果需要,就按参数mode中给出的访问模式创建文件。
O_EXCL:与O_CREAT一起使用,确保调用者创建出文件。open是一个原子操作,也就是说,它只执行一个函数调用。使用这个可选模式可以防止两个程序同时创建一个文件。如果文件已经存在,open调用将失败。
关于其他可能出现的oflag值,请参考open调用的使用手册,它们出现在该手册的第二小节(使
用man 2 open命令查看)。
open调用在成功时返回一个新的文件描述符(它总是一个非负整数),失败时返回-1,并设置全局变量errno以指明失败的原因。我们将在下一小节对errno做进一步讨论。新文件描述符总是使用未用描述符的最小值,这个特征在某些情况下是非常有用的。例如,如果一个程序关闭了自己的标准输出,然后再次调用open,文件描述符l就会被重新使用,并且标准输出将被有效地重定向到另一个文件或设备。
POSIX规范还标准化了一个creat调用,但它并不常用。这个调用不仅会创建文件,还会打开文件——它的作用相当于以oflags标志O_CREAT | O _ WRONLY |
O_TRUNC来调用open。
*
访问权限的初始值
当使用带有O_CREAT标志的open来创建文件时,我们必须使用有三个参数格式的open调用。第三个参数mode是几个标志按位OR后得到的,这些标志在头文件sys/stat.h.中定义,它们是:
S_IRUSR:读权限,文件属主。
S_IWUSR:写权限,文件属主。
S_IXUSR:执行权限,文件属主。
S_IRGRP:读权限,文件所属组。
S_IWGRP:写权限,文件所属组。
S_IXGRP:执行权限,文件所属组。
S_IROTH:读权限,其他用户。
S_lW0TH:写权限,其他用户。
S_IXOTH:执行权限,其他用户。
请看下面的例子:
open (“myfile”, O_CREAT, S_IRUSR|S_IXOTH);
$ ls -ls myfile
0 -r-------x 1 neil software 0 Sep 22 08:11 myfile*
有几个因素会对文件的访问权限产生影响。首先,只有在创建文件时才会指定访问权限。其次,用户掩码(由shell的umask命令设定)会影响到被创建文件的访问权限。open调用里给出的模式值将与当时的用户掩码的反值做AND操作。举例来说,如果用户掩码被设置为001,并且指定了S_IXOTH模式标志,那么其他用户对创建的文件不会拥有执行权限,因为用户掩码中指定了不允许向其他用户提供执行权限。因此,open和creat调用中的标志实际上是设置文件访问权限的请求,所请求的权限是否会被设置取决于当时umask的值。
1. umask变量(略)
2. close系统调用
我们使用close调用终止一个文件描述符fildes与其对应文件之间的关联。文件描述符被释放并能够重新使用。close调用成功就返回0,出错就返回-1。
#include
int close(int
fildes);
注意,有时检查close调用的返回结果十分重要。有的文件系统,特别是网络文件系统,可能不会在关闭文件之前报告文件写操作中出现的错误,因为执行写操作时,数据可能未被确认写入。
运行中的程序能够一次打开的文件数目是有限制的。这个限制由头文件limits.h中的OPEN_MAX常数定义,它会随着系统的不同而不同,但POSIX规范要求它至少要为16。这个限制本身还会受到本地系统全局性限制的影响。
3.ioctl系统调用
ioctl调用有点像是个大袋子。它提供了一个用于控制设备及其描述符行为和配置底层服务的接口。终端、文件描述符、套接字甚至磁带机都可以有为它们定义的ioctl,具体细节可以参考特定设备的使用手册。POSIX规范只为流(stream)定义了ioctl调用,对它的讨论超出了本书的讨论范围。下面是ioctl的原型:
#include
int ioctl(int fildes, int cmd,
...);
ioctl对描述符fildes指定的对象执行cmd参数中给出的操作。根据特定设备所支持函数的不同,还可能会有一个可选的第三参数。
实验:一个文件拷贝程序
# vi copy_system.c
#include
#include
#include
#include
int main()
{
char c;
int in, out;
in = open("file.in",
O_RDONLY);
out =
open("file.out", O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);
while(read(in,&c,1) == 1)
write(out,&c,1);
exit(0);
}
注意,#include
到其他的头文件。
首先,我们需要有个用于测试的输入文件,长度为1MB,取名为file.in。
运行这个程序,将给出如下的输出结果:
#TIMEFORMAT="" time
./copy_system
0.07user 1.52system
0:01.60elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs
(0major+85minor)pagefaults 0swaps
我们在这里使用time工具对这个程序运行的时间进行了测算。Linux使用TIMEFORMAT变量来重置默认的POSIX时间输出格式,POSIX时间格式不包括CPU使用率。我们可以看到在这台相当老的系统上,1MB的输入文件file.in被成功拷贝到file.out,file.out以只允许属主进行读写的权限被创建出来。但这次拷贝花费了大约两分半钟,并且几乎消耗了所有的CPU时间。之所以这么慢,是因为它必须完成超过两百万次的系统调用。
近些年来,Linux在系统调用和文件系统性能方面有了很大改善。同样的测试在Linux 2.4内核下只需不到5秒就完成了。上面的例子仅仅耗时1s多。
我们可以通过拷贝大一些的数据块来改善效率较低的问题,请看下面这个改进后的程序copy_block.c,它每次拷贝长度为IK的数据块,用的还是系统调用:
# vi copy_block.c
#include
#include
#include
#include
int main()
{
char block[1024];
int in, out;
int nread;
in = open("file.in", O_RDONLY);
out = open("file.out",
O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);
while((nread =
read(in,block,sizeof(block))) > 0)
write(out,block,nread);
exit(0);
}
运行结果:
#IMEFORMAT="" time ./copy_block
0.00user 0.00system 0:00.00elapsed 100%CPU
(0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+85minor)pagefaults 0swaps
改进后的程序只花费了0.01秒的时间,因为它只需做大约2000次系统调用。当然,这些时间与系统本身的性能有很大的关系,但它们确实显示了系统调用需要巨大的开支,因此值得对其使用进行优化。
*
其他与文件管理有关的系统调用
还有许多其他的系统调用能够对这些底层文件描述符进行操作。它们允许程序对文件的使用方式和返回的状态信息进行控制。
1.lseek系统调用
lseek系统调用对文件描述符fildes的读写指针进行设置,也就是说,可以用它来设置文件的下一个读写位置。读写指针既可设置为文件中的某个绝对位置,也可以把它设置为相对于当前位置或文件尾的某个相对位置。
#include
#include
off_t lseek(int fildes, off_t offset, int whence);
offset参数用来指定位置,而whence参数定义该偏移值的用法。whence可以取下列值之一:
SEEK_SET:offset是一个绝对位置。
SEEK_CUR:offset是相对于当前位置的一个相对位置。
SEEK_END:offset是相对于文件尾的一个相对位置。
lseek返回从文件头到文件指针被设置处的字节偏移值,失败时返回-1。参数offset的类型off_t是一个与具体实现有关的类型,它定义在文件sys/types.h中。
2.fstat、stat和lstat系统调用
fstat系统调用返回与打开的文件描述符相关的文件的状态信息,该信息将会写到buf结构中,buf的地址以参数形式传递给fstat。
下面是它的原型:
#include
#include
#include
int fstat(int fildes, struct stat
*buf);
int stat(const char *path, struct stat
*buf);
int lstat(const char *path, struct stat *buf);
注意:包含头文件sys/types.h是可选的,但由于一些系统调用在使用标准类型时采用的是别名方式,而这种方式可能在以后会改变,所以在使用系统调用时,我们推荐将这个头文件包含进去。
相关的函数stat和lstat返回的是通过文件名查到的状态信息。它们的结果基本一致,但当文件是一个符号链接时,lstat返回的是该符号链接本身的信息,而stat返回的是该链接指向的文件的信息。stat结构的成员在不同的UNIX系统上会有所变化,但一般会包括表3-4中所示的内容。
stat成员 说 明
St_mode 文件权限和文件类型信息
st_ino 与该文件关联的inode
st_dev 保存文件的设备
st_uid 文件属主的UID号
st_gid 文件属主的GID号
st_atime 文件上一次被访问的时间
st_ctime 文件的权限、属主、组或内容上一次被改变的时间
st_mtime 文件的内容上一次被修改的时间
st_nlink 该文件上硬连接的个数
stat结构中返回的st_mode标志还有一些与之关联的宏,它们定义在头文件sys/stat.h中。这些宏包括对访问权限、文件类型标志以及一些用于帮助测试特定类型和权限的掩码的定义。
访问权限标志与前面的open系统调用中的内容是一样的。文件类型标志包括:
S_IFBLK:文件是一个特殊的块设备。
S_ IFDIR:文件是一个目录。
S_IFCHR:文件是一个特殊的字符设备。
S_IFIFO:文件是一个FIFO设备(命名管道)
S_IFREG:文件是一个普通文件。
S_FLNK:文件是一个符号链接。
其他模式标志包括:
S_ISUID:文件设置了SUID位。
S_ISGID:文件设置了SGID位。
用于解释st_mode标志的掩码包括:
S_IFMT:文件类型。
S_IRWXU:属主的读/写/执行权限。
S_IRWXG:属组的读/写/执行权限。
S_IRWXO:其他用户的读/写/执行权限。
还有一些用来帮助确定文件类型的宏定义。它们只是对经过掩码处理的模式标志和相应的设备类
型标志进行比较。它们包括:
S_ISBLK:测试是否是特殊的块设备文件。
S_ISCHR:测试是否是特殊的字符设备文件。
S_ISDIR:测试是否是目录。
S_ISFIFO:测试是否是FIFO设备。
S_ISREG:测试是否是普通文件。
S_ISLNK:测试是否是符号链接。
例如,如果想测试一个文件“代表的不是一个目录,设置了属主的执行权限,不再有其他权限”,
我们可以使用如下的代码进行测试:
struct stat statbuf;
mode_t modes;
stat(“filename”,&statbuf);
modes = statbuf.st_mode;
if(!S_ISDIR(modes) && (modes & S_IRWXU) ==
S_IXUSR)
...
3.dup和dup2系统调用
dup系统调用提供了复制文件描述符的一种方法,使我们能够通过两个或者更多个不同的描述符来访问同一个文件。这可以用于在文件的不同位置对数据进行读写。dup系统调用复制文件描述符fildes,返回一个新的描述符。dup2系统调用是通过明确指定目标描述符来把一个文件描述符复制为另外一个。
其原型如下:
#include
int dup(int fildes);
int dup2(int fildes, int fildes2);
当我们通过管道在多个进程间进行通信时,这些调用也很有用。我们将在第13章对dup系统调用进行深入研究。