木头骑士的Linux编程实验室(三)——文件描述符的操作

上一篇文章我们实验了几个基本的文件操作,它们大多以文件描述符作为操作对象,来操作一个文件。这一篇中,我们将对文件描述符本身作为测试对象,实验文件描述符的操作。首先,我们先看看文件描述符在系统中处于一个什么样的地位,贴上一张《Linux/UNIX系统编程手册》 的图:


图中最左边的是进程,每个进程都会维护一个文件描述符表,当打开一个新文件时,文件描述符表中的未使用的最小文件描述符就会指向文件表中表示相应文件的一个记录行。这个打开文件表是一个系统级的表,用于记录打开的文件。而打开的文件由指向i-node表中的一项表示,i-node中的一项用于记录文件系统中的一个文件,我们将在下一篇做详细实验。图中可以看出,同一进程可以多次打开同一个文件(多个文件描述符指向同一文件表项),不同进程也可以同时打开同一文件,这里面的同步互斥问题,我们也将在后续的文章中进行实验。
本章仅对文件描述符本身进行一些操作。
1.文件描述符的复制

在前面的图中可见,同一进程中的两个文件描述符可以指向同一个文件表项,那么是不是一定要打开文件两次,返回两个相同的文件描述符呢。不一定。UNIX中提供了专门用于复制文件描述符的系统调用,也就是说,我们打开一个文件描述符之后,可以再复制一个。这两个系统调用的原型如下:
#include <unistd.h>
int dup(int d);
int dup2(int fd, int fd2);
        返回值:若成功,返回新的文件描述符;若出错,返回-1
dup返回的新文件描述符一定是当前可用文件描述符中的最小值。
dup2可以用fd2参数指定新描述符的值。如果fd2已经打开,则先将其关闭。如果fd等于fd2,则dup2返回fd2,而不关闭它。
为了实验这两个系统调用,我们可以利用系统已经为我们打开的那三个文件描述符——标准输入,标准输出和标准出错,他们的文件描述符分别是0、1、2。
首先做这样一个实验,我们用dup复制一个标准输出的文件描述符,然后向得到的新文件描述符中写入字符串,观察输出情况。
void dupTest(){
	int newfd = dup(1);
	if (-1 == newfd) {
		perror("dup error");
		return;
	}
	if (-1 == write(newfd, "Hello World.", strlen("Hello World."))) {
		perror("write error");
		return;
	}
	if (-1 == close(newfd)) {
		perror("close error");
		return;
	}
}
运行这段代码,屏幕打印出Hello World,说明我们向复制的文件描述符newfd中写入,相当于写到了标准输出中。
dup2常用来做输入输出的重定向,比如将标准出错重定向到标准输出,在shell中可以用如下命令实现:2>&1,表明将文件描述符2重定向到文件描述符1,我们可以用dup2做如下实现:
dup2(1, 2);
上面这句会先将文件描述符2关闭,然后复制文件描述符1到文件描述符2。
下面试验先将将标准出错重定向到标准输出,然后使用第一篇文章中介绍过的errorTest()函数产生一个出错输出,然后执行./LinuxTest>/tmp/a.txt,将编译好的文件执行输出重定向到文件,发现终端没有输出,输出都写到了文件/tmp/a.txt中,表明已经将标准出错重定向到了标准输出。代码如下:
void dup2Test(){
	if (-1 == dup2(1, 2)) {
		perror("dup2 error");
		return;
	}
	errorTest();
}
2.函数fcntl

fcntl系统调用用于获取或设置一个打开的文件描述符的属性。
#include <fcntl.h>
int fcntl(int fd, int cmd, ... );
        返回值:若成功,返回值依赖于cmd;若出错,返回-1
fcntl函数的cmd可以分为5组:
  1. 复制一个已有的描述符(cmd = F_DUPFD或F_DUPFD_CLOEXEC)。
  2. 获取/设置文件描述符标志(cmd = F_ETFD或F_SETFD)。
  3. 获取/设置文件描述符状态标识(cmd = F_GETFL或F_SETFL)。
  4. 获取/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN)。
  5. 获取/设置记录锁(cmd = F_GETLK、F_SETLK或F_SETLKW)。
下面针对这些个cmd分别进行试验。
2.1复制文件描述符

前面见到dup可以复制一个文件描述符,其返回的描述符是当前可用的最小描述符。fcntl用FD_DUPFD可以通过第三个参数指定一个最小的描述符,所以dup(fd)等价于fcntl(fd, F_DUPFD, 0)。所以我们可以用fcntl的F_DUPFD重做前面的dupTest()实验。

void fcntlTest1() {
	int newfd = fcntl(1, F_DUPFD, 0);
	if (-1 == newfd) {
		perror("dup error");
		return;
	}
	if (-1 == write(newfd, "Hello World.", strlen("Hello World."))) {
		perror("write error");
		return;
	}
	if (-1 == close(newfd)) {
		perror("close error");
		return;
	}
}
这段代码中,用fcntl(1, F_DUPFD, 0)替换了dup(1),其效果是一样的。
F_DUPFD和F_DUPFD_CLOEXEC的作用都是复制文件描述符,但前者复制的文件描述符在exec时仍保持有效,而后者复制的文件描述符在exec时就不可用了。这里得稍稍做点解释,因为我们还没有做多进程的试验嘛。Linux的多进程是通过fork()函数实现的,fork函数会复制一个进程,复制出的子进程与父进程拥有同样的资源,同样的文件描述等,fork之后可以用一系列exec函数将子进程替换为另一个可执行文件,那么此时继承过来的文件描述符是否还可用呢。答案是,看该文件描述符是否设置了FD_CLOEXEC标志,如果设置了该标志,则文件描述符在exec后不可用,否则可用。该标志可以在打开文件时,通过open函数指定oflag参数的O_CLOEXEC标志可以在打开文件时设置FD_CLOEXEC标志,也可以通过fcntl函数的F_SETFD这一cmd来设置,还有就是现在要说的,在复制文件描述符时通过F_DUPFD_CLOEXEC来指定。下面试验对比F_DUPFD和F_DUPFD_CLOEXEC。因为涉及到多进程,涉及到exec,所以要编译出两个可执行文件,一个用于测试,一个用于子进程。首先是测试代码:
void fcntlTest2() {
	int newfd1 = fcntl(1, F_DUPFD, 10);
	if (-1 == newfd1) {
		perror("dup error");
		return;
	}
	int newfd2 = fcntl(1, F_DUPFD_CLOEXEC, 11);
	if (-1 == newfd2) {
		perror("dup error");
		return;
	}
	printf("newfd1: %d\n", newfd1);
	printf("newfd2: %d\n", newfd2);

	if (0 == fork()){
		execl("/tmp/a.out", NULL);
	}

	sleep(1);
	if (-1 == close(newfd1)) {
		perror("close error");
		return;
	}
	if (-1 == close(newfd2)) {
		perror("close error");
		return;
	}
}
在测试代码中,我们用文件描述符10,以F_DUPFD的方式复制一个标准输出的文件描述符,然后用文件描述符11以F_DUPFD_CLOEXEC的方式再复制一个标准输出。之后fork一个子进程,在子进程中调用execl,用/tmp/a.out这一程序替换子进程。
在来看看/tmp/a.out做了什么:
int main() {
	if (-1 == write(10, "child process write to fd 10\n", strlen("child process write to fd 10\n"))) {
		perror("child 10 write error");
		return -1;
	}
	if (-1 == write(11, "child process write to fd 11\n", strlen("child process write to fd 11\n"))){
		perror("child 11 write error");
		return -1;
	}
}
在子进程中,我们分别向文件描述符10和文件描述符11写入字符串,这两个文件描述符都是父进程打开的,看看运行结果怎样:
child process write to fd 10
child 11 write error: Bad file descriptor
newfd1: 10
newfd2: 11
可以看到,文件描述符10在子进程中仍然可以写入,而文件描述符11则已经不可用了,表明fcntl函数通过F_DUPFD复制的文件描述符在exec后仍然可用,而通过F_DUPFD_CLOEXEC复制的文件描述符在exec后不可用了。
探讨到这里,突然想到个问题呢,如果是用dup复制的文件描述符,在子进程exec后是否可用呢。之前说过,dup(fd)等价于fcntl(fd, F_DUPFD, 0),也就是说,仍然是可用的。用下面试验来证实这一说法:
void dupTest2(){
	int newfd = dup(1);
	if (-1 == newfd) {
		perror("dup error");
		return;
	}
	printf("newfd1: %d\n", newfd);
	if (0 == fork()){
		execl("/tmp/a.out", NULL);
	}
	if (-1 == close(newfd)) {
		perror("close error");
		return;
	}
}
这段代码中,用dup(1)复制一个文件描述符,一般来说,这个得到的文件描述符是3了。然后在子进程中执行/tmp/a.out,此时的/tmp/a.out为如下程序:

int main() {
	if (-1 == write(3, "child process write to fd 3\n", strlen("child process write to fd 3\n"))) {
		perror("child 3 write error");
		return -1;
	}
}

向文件描述符3中写入字符串,运行结果如下:
newfd1: 3
child process write to fd 3
可见,文件描述符3在子进程中仍然是可用的。
2.2 获取/设置文件描述符标志
F_GETFD和F_SETFD两个cmd分别用于获取和设置文件描述符标志。目前只有一个标志可用,就是FD_CLOEXEC,在上一小节提过了,这个东东用于标识在fork一个子进程后,子进程调用exec后该文件描述符是否仍然可用。下面程序以不设置O_CLOEXEC的形式打开a.txt,返回文件描述符fd1,以设置了O_CLOEXEC的形式打开b.txt,返回文件描述符fd2,用fcntl的F_GETFD命令获取两者的文件描述符标识,并打印出来,然后用fcntl的F_SETFD标识将fd1的FD_CLOEXEC打开,将fd2的FD_CLOEXEC关闭,再次获取两者的文件描述符标识,并打印出来。
代码如下:
void fcntlTestGETFD_SETFD(){
	int fd1 = open("/tmp/a.txt", O_RDONLY);
	if (-1 == fd1) {
		perror("open fd1 error");
		return;
	}
	int fd1Attr = fcntl(fd1, F_GETFD);
	if (-1 == fd1Attr) {
		perror("fcntl fd1 error");
	}
	if (FD_CLOEXEC == fd1Attr) {
		printf("fd1: FD_CLOEXEC set\n");
	}
	else {
		printf("fd1: FD_CLOEXEC not set\n");
	}

	int fd2 = open("/tmp/b.txt", O_RDONLY | O_CLOEXEC);
	if (-1 == fd2) {
		perror("open fd2 error");
		return;
	}
	int fd2Attr = fcntl(fd2, F_GETFD);
	if (-1 == fd2Attr) {
		perror("fcntl fd2 error");
	}
	if (FD_CLOEXEC ==fd2Attr) {
		printf("fd2: FD_CLOEXEC set\n");
	}
	else {
		printf("fd2: FD_CLOEXEC not set\n");
	}

	// set
	if (-1 == fcntl(fd1, F_SETFD, fd1Attr | FD_CLOEXEC)) {
		perror("fcntl fd1 F_SETFD error");
	}
	if (-1 == fcntl(fd2, F_SETFD, fd2Attr & ~FD_CLOEXEC)) {
		perror("fcntl fd2 F_SETFD error");
	}


	printf("After F_SETFD\n");

	fd1Attr = fcntl(fd1, F_GETFD);
	if (-1 == fd1Attr) {
		perror("fcntl fd1 error");
	}
	if (FD_CLOEXEC == fd1Attr) {
		printf("fd1: FD_CLOEXEC set\n");
	}
	else {
		printf("fd1: FD_CLOEXEC not set\n");
	}

	fd2Attr = fcntl(fd2, F_GETFD);
	if (-1 == fd2Attr) {
		perror("fcntl fd2 error");
	}
	if (FD_CLOEXEC ==fd2Attr) {
		printf("fd2: FD_CLOEXEC set\n");
	}
	else {
		printf("fd2: FD_CLOEXEC not set\n");
	}

	close(fd1);
	close(fd2);
}
执行结果为:
fd1: FD_CLOEXEC not set
fd2: FD_CLOEXEC set
After F_SETFD
fd1: FD_CLOEXEC set
fd2: FD_CLOEXEC not set
在这一实验中,改变文件描述符标志的方法是先将文件描述符标识读出来,然后以位操作的方式置位和复位FD_CLOEXEC,但看了一下linux的FD_CLOEXEC的定义,在fcntl.h中定义如下:
<span style="font-size:14px;">/* For F_[GET|SET]FD.  */
#define FD_CLOEXEC	1	/* actually anything with low bit set goes */</span>
而且关于文件描述符标志的定义只此一处,也就是说,在目前的实现中,只要设置为0,就关闭了FD_CLOEXEC,设置为1就开启FD_CLEXEC,但为了与以后可能出现的变化兼容,还是小心使用位操作吧。
2.3 获取/设置文件状态标志
F_SETFL和F_GETFL分别用于获取和设置文件状态标志,文件状态标志可以在用open打开一个文件时指定。有如下可用的标志(表格摘自《APUE》):

由于历史原因(前三个值历史上分别为0,1,2),前5个值不能通过按位比较的方式判断,必须读出文件状态标志之后,与O_ACMODE做按位与,然后分别于5个访问标志进行对比(O_RDONLY、O_WRONLY、O_RDWR、O_EXEC以及O_SEARCH)来判断,这5个标志只能取其一。其他的标志则可以用得到的结果与相应的标志做按位与加以判断。
对于F_SETFL命令,并不能改变所有标志,能够改变的标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC和O_ASYNC。
获取文件状态标识,《APUE》上有个很好的例子。因为我没有包含书中提供的apue.h,所以代码有小小的修改。
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
	int val;
	
	char err[] = "usage: a.out <descriptor#>\n";
	if (argc != 2) {
		write(2, err, strlen(err));
		return -1;
	}
	
	if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
		perror("fcntl error");

	switch (val & O_ACCMODE) {
		case O_RDONLY:
			printf("read only");
			break;
		case O_WRONLY:
			printf("write only");
			break;
		case O_RDWR:
			printf("read write");
			break;
		default:
			printf("read write");
	}

	if (val & O_APPEND)
		printf(", append");
	if (val & O_NONBLOCK)
		printf(", nonblocking");
	if (val & O_SYNC)
		printf(", synchronous writes");

#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
	if (val & O_FSYNC)
		printf(", synchronous writes");
#endif
	putchar('\n');
	exit(0);
}
对程序执行如下测试:
$./a.out 0 < /dev/tty
read only
$./a.out 1 > temp.foo
$cat temp.foo
write only
$./a.out 2 2>>temp.foo
write only, append
$./a.out 5 5<>temp.foo
read write
试验中,将一个文件描述符作为参数传入,第一次测试,将标准输入重定向到/dev/tty,并且将文件描述符0作为参数传入,得到结果是read only;第二次测试,将标准输出重定向到temp.foo,并将文件描述符1作为参数传入,得到结果是write only;第三次测试,将标准出错以追加的形式重定向到temp.foo,并将文件描述符2作为参数传入,得到结果是write only, append;第四次测试,在文件描述符5上以读写的形式打开temp.foo,并将文件描述符5作为参数传入,得到结果是read write。
之后再来实验一个设置文件状态标志的例子,《APUE》提供了如下代码段用于置位一个文件状态标志:
void set_fl(int fd, int flags){
	int val;
	if ((val = fcntl(fd, F_GETFL, 0)) < 0)
		perror("fcntl F_GETFL error");
	val |= flags;	// turn on flags
	if (fcntl(fd, F_SETFL, val) < 0)
		perror("fcntl F_SETFL error");
}
设置方式是先将文件状态标志读出来,再与要设置的标志做按位或,类似方法,可以写出清除一个文件状态标志的函数:
void clr_fl(int fd, int flags){
	int val;
	if ((val = fcntl(fd, F_GETFL, 0)) < 0)
		perror("fcntl F_GETFL error");
	val &= ~flags;	// turn off flags
	if (fcntl(fd, F_SETFL, val) < 0)
		perror("fcntl F_SETFL error");
}
方法是先将文件状态标志读出来,再要设置的标志的按位取反做按位与。
下面再写一段代码来测试,测试中,用O_APPEND和O_NONBLOCK两个标志做测试,先已O_APPEND的形式打开文件,然后将该标志清除,设置O_NONBLOCK标志,观察前后变化。代码如下:
void fcntlTestGETFL_SETFL(){
	int fd = open("/tmp/a.txt", O_RDWR | O_APPEND);
	if (-1 == fd) {
		perror("open error");
	}
	// get file status
	int val;
	if ((val = fcntl(fd, F_GETFL, 0)) < 0)
		perror("fcntl F_GETFL error");
	if (val & O_APPEND)
		printf("append\n");
	if (val & O_NONBLOCK)
		printf("nonblock\n");

	set_fl(fd, O_NONBLOCK);		// set O_NONBLOCK
	clr_fl(fd, O_APPEND);		// clear O_APPEND

	// get file status now
	if ((val = fcntl(fd, F_GETFL, 0)) < 0)
		perror("fcntl F_GETFL error");
	printf("after set O_NONBLOCK and clear O_APPEND\n");
	if (val & O_APPEND)
		printf("append\n");
	if (val & O_NONBLOCK)
		printf("nonblock\n");
}
运行结果为:
append
after set O_NONBLOCK and clear O_APPEND
nonblock
后面还有两组:设置/获取异步I/O所有权和设置/获取记录锁,这两部分在后面实验到异步I/O和记录锁时在进行研究。

说明:本文的实验使用eclipse建立工程,已上传至github:
https://github.com/haoranzeus/LinuxProgrammingLib.git

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值