UNIX再学习 -- 文件I/O

上一篇讲完文件描述符,接下来进入正题,文件的处理函数

一、函数 open

详细内容,可自行 man creat 查看
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

int creat(const char *pathname, mode_t mode);

1、参数解析

第一个参数:字符串形式的路径和文件名
第二个参数:操作标志
O_RDONLY    只读模式
O_WRONLY    只写模式
O_RDWR      读写模式
O_EXEC      只执行打开
O_SEARCH    只搜索打开(应用于目录)
上述 5 种标志位是互斥的,也就是不可同时使用,但可与下列的标志位利用 OR(|) 运算符组合。
O_APPEND:每次写时追加到文件的尾端。
O_CLOEXEC:把FD_CLOEXEC常量设置为文件描述符标志。
O_CREAT:若此文件不存在则创建它。使用此选项时,open函数需同时说明第3个参数mode(openat函数需说明第4个参数mode),用mode指定该新文件的访问权限位。
O_DIRECTORY:如果path引用的不是目录,则出错。
O_EXCL:如果同时指定了O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者称为一个原子操作。
O_NOCTTY:如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端。
O_NOFOLLOW:如果path引用的是一个符号链接,则出错。
O_NONBLOCK:如果path引用的是一个FIFO、一个特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后序的I/O操作设置非阻塞方式。
O_SYNC :使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O。
O_TRUNC:如果此文件存在,而且为只读或读-写成功打开,则将其长度截断为0。
O_TTY_INIT:如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single UNIX Specification(以及POSIX.1)中同步输入和输出选项的一部分。
O_DSYNC:使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新。
O_RSYNC:使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成。
第三个参数:操作模式
仅当创建新文件时才使用,用于指定创建的新文件权限。可以使用宏定义或者八进制文件权限码。
注:此为Linux2.2以后特有的标志位,以避免一些系统安全问题。参数mode 则有下列数种组合,只有在建立新文件时才会生效,此外真正建文件时的权限会受到umask值所影响,因此该文件权限应该为(mode-umaks)。

(1)八进制文件权限码(常用)

# ls -la
总用量 20
drwxrwxr-x 2 tarena tarena 4096 Mar 27 14:44 .
drwxrwxr-x 4 tarena tarena 4096 Mar 14 11:14 ..
-rwxr-xr-x 1 root   root   7158 Mar 27 14:44 a.out
-rw-r--r-- 1 root   root     79 Mar 27 14:44 test.c
以 test.c 为例:
- 代表文件类型  
rw 代表属主
r 代表属组
r 代表其它
1 代表硬链接数
root 代表属主名称
root 代表属组名称
79 代表文件大小
Mar 27 14:44 代表文件最后修改时间
test.c 代表文件名
按照:0 表示没有权限,1 表示可执行权限,2 表示可写权限,4 表示可读权限,然后将其相加。所以数字属性的格式应为 3 个从 0 到 7 的八进制数,其顺序是文件属主(u)、与文件属组(g)、其他用户(o)
因此上面,test.c 的权限为 0644
注:可以在程序中用八进制方式表示数字必须以 0 作为开头,采用 %o 作为占位符可以把一个整数的八进制表示方式打印在屏幕上 。

(2)宏定义方式

可查看 /usr/include/linux/stat.h 看到具体定义
S_IRWXU, 00700权限,代表该文件所有者具有可读、可写及可执行的权限。
S_IRUSR 或 S_IREAD, 00400权限,代表该文件所有者具有可读取的权限。
S_IWUSR 或 S_IWRITE,00200权限,代表该文件所有者具有可写入的权限。
S_IXUSR 或 S_IEXEC, 00100权限,代表该文件所有者具有可执行的权限。

S_IRWXG  00070权限,代表该文件用户组具有可读、可写及可执行的权限。
S_IRGRP  00040权限,代表该文件用户组具有可读的权限。
S_IWGRP  00020权限,代表该文件用户组具有可写入的权限。
S_IXGRP  00010权限,代表该文件用户组具有可执行的权限。

S_IRWX   O00007权限,代表其他用户具有可读、可写及可执行的权限。
S_IROTH  00004权限,代表其他用户具有可读的权限
S_IWOTH  00002权限,代表其他用户具有可写入的权限。
S_IXOTH  00001权限,代表其他用户具有可执行的权限。
同八进制方法格式一样,不过就显得尤为复杂了,例如:
设置权限 0644:S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH

2、返回值:

成功返回一个新的文件描述符,失败返回 -1。文件描述符就是非负的整数,用于代表一个打开的文件。

3、错误代码

EEXIST 参数pathname 所指的文件已存在, 却使用了O_CREAT 和O_EXCL 旗标.
EACCESS 参数pathname 所指的文件不符合所要求测试的权限.
EROFS 欲测试写入权限的文件存在于只读文件系统内.
EFAULT 参数pathname 指针超出可存取内存空间.
EINVAL 参数mode 不正确.
ENAMETOOLONG 参数 pathname 太长.
ENOTDIR 参数pathname 不是目录.
ENOMEM 核心内存不足.
ELOOP 参数pathname 有过多符号连接问题.
EIO I/O 存取错误.

4、函数功能

打开/创建 一个文件/设备。

5、与 openat 函数区别

可自行 man openat 查看 openat 的详细内容,在此不多讲解了。
openat 函数是 POSIX.1 最新版本中新增的一类函数之一,希望解决两个问题。
第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。
第二,可以避免time-of-check-to-time-of-use(TOCTTOU错误

6、与 creat 函数区别

调用creat函数创建一个新文件。
返回值:若成功,返回只写打开的文件描述符;若出错,返回-1。
此函数等效于:
open (path, O_RDWR | O_CREAT | O_TRUNC, mode);

二、函数 close

#include <unistd.h>
int close(int fd);

1、函数功能:

close()关闭一个文件描述符,所以它不再引用任何文件,可以重用。 任何记录锁(见fcntl(2))举行与该进程关联并拥有的文件被删除(不管用于获取的文件描述符)锁)。
如果 fd 是引用底层打开文件描述的最后一个文件描述符(见open(2)),则与打开的文件描述被释放; 如果描述符是使用 unlink(2)文件删除的文件的最后引用被删除。

2、示例说明

//open 函数和 close 函数的使用
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main (void)
{
//文件不存在则创建,存在则打开
//	int fd = open ("a.txt", O_RDONLY | O_CREAT, 0644);

//文件不存在则创建,存在则创建失败	
//	int fd = open ("a.txt", O_RDONLY | O_CREAT | O_EXCL, 0644);

//文件存在,且为普通文件,打开方式有写权限,则清空文件 
	int fd = open ("a.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
	if (-1 == fd)
		perror ("open"), exit (-1);
	printf ("打开文件成功!\n");
	
	
	printf("fd = %d\n", fd);

	int res = close (fd);
	if (-1 == res)
		perror ("cosee"), exit (-1);
	printf("关闭文件成功!\n");
	return 0;
}
输出结果:
打开文件成功!
fd = 3
关闭文件成功!

三、函数 lseek

每个打开文件都有一个与其相关联的“当前文件偏移量”。它通常是一个非负数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定 O_APPEND 选项,否则该偏移量被设置为 0.
可以调用 lseek 显式地为一个打开文件设置偏移量。
#include <sys/types.h>
#include <unistd.h>
off_t lseek (int fd, off_t offset, int whence);

1、参数解析

第一个参数:文件描述符
第二个参数:偏移量
第三个参数:从什么地方开始偏移
若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。 
若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。 
若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。

偏移起始位置:文件头0(SEEK_SET),当前位置1(SEEK_CUR),文件尾2(SEEK_END))为基准,偏移offset(指针偏移量)个字节的位置。

2、返回值

若成功,返回新的文件偏移量;若出错,返回为 -1。为此可以用下列方式确定打开文件的当前偏移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
这种方法也可用来确定所涉及的文件是否可以设置偏移量。如果文件描述符指向的是一个管道、FIFO或网络套接字,则 lseek 返回 -1,并将 errno 设置为 ESPIPE (Illegal seek)。

3、函数功能

表示调整文件的读写位置。

4、示例说明

//示例一
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>

int main (void)
{
	int fd = open ("a.txt", O_RDWR);
	if (-1 == fd)
		perror ("open"), exit (-1);
	printf ("打开文件成功!\n");

	printf ("ABCDEFGHIJKLMN\n");

	char c;
	//第一次读 从开头读取 A
	read (fd, &c, sizeof (char));
	printf ("c = %c\n", c);
	//第二次读,移到下一个 B
	read (fd, &c, sizeof (char));
	printf ("c = %c\n", c);
	printf ("-------------------\n");

	//SEEK_CUR 当前位置读取 (2+0 = 2)(CDE)
	lseek (fd, 2L, SEEK_CUR);
	read (fd, &c, sizeof (char));
	printf ("c = %c\n", c);
	
	//SEEK_CUR 当前位置读取(-2+0 = -2) (DEF)
	lseek (fd, -2L, SEEK_CUR);
	read (fd, &c, sizeof (char));
	printf ("c = %c\n", c);
	
	//SEEK_SET 从开头位置读取,(3 + 1 = 4)(ABCD)
	lseek (fd, 3L, SEEK_SET);
	read (fd, &c, sizeof (char));
	printf ("c = %c\n", c);
	
	//SEEK_SET 从开头位置读取,(-5 + 1 = -4)错误,指向当前位置
	lseek (fd, -5L, SEEK_SET);
	read (fd, &c, sizeof (char));
	printf ("c = %c\n", c);
	
	//从结尾位置读取,(-3 + 2 = -1) (MN)
	lseek (fd, -3L, SEEK_END);
	read (fd, &c, sizeof (char));
	printf ("c = %c\n", c);

	close (fd);
	printf ("关闭文件成功!\n");
	return 0;
}
输出结果:
打开文件成功!
ABCDEFGHIJKLMN
c = A
c = B
-------------------
c = E
c = D
c = D
c = E
c = M
关闭文件成功!
//示例二
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	if (lseek (STDIN_FILENO, 0, SEEK_CUR) == -1)
		printf ("cannot seek\n");
	else 
		printf ("seek OK\n");
	return 0;
}
使用重定向测试:
# ./a.out < /etc/passwd
seek OK

# cat < /etc/passwd | ./a.out 
cannot seek

# ./a.out < /var/spool/cron/FIFO
cannot seek
//示例三
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>

int main (void)
{
	int fd = open ("a.txt", O_RDWR | O_APPEND);
	//nt fd = open ("a.txt", O_RDWR);
	if (-1 == fd)
		perror ("open"), exit (-1);
	printf ("打开文件成功!\n");

	char c;
	lseek (fd, 3, SEEK_SET);
	read (fd, &c, sizeof (char));
	printf ("c = %c\n", c);
	
	char buffer[5] = "1234";
	if(write(fd,buffer,strlen(buffer)) !=strlen(buffer))
		perror("write error"), exit(-1);
	close (fd);

	return 0;
}
查看 a.txt
# cat a.txt 
ABCDEFJHIJKLMN
1234
说明:
(1)示例一,说明偏移起始位置:文件头0(SEEK_SET),当前位置1(SEEK_CUR),文件尾2(SEEK_END))为基准,偏移offset(指针偏移量)个字节的位置。lseek()函数允许将文件偏移量设置为超出文件结尾(但不会更改文件的大小)。 如果偏移量超出范围,则打印当前位置。
(2)示例二,说明如果文件描述符指向的是一个管道、FIFO或网络套接字,则 lseek 返回 -1。 比较 lseek 的返回值时,不要测试它是否小于 0,而要测试它是否等于 -1。
(3)示例三, 说明 按系统默认的情况 ,当打开一个文件时,除非指定  O_APPEND  选项,否则该偏移量被设置为 0。 O_APPEND的含义是在每次写之前,都讲标志位移动到文件的末端。而  O_APPEND 打开后,是一个原子操作:移动到末端,写数据,跟位移 lseek 无关。

5、与 fseek 区别

int fseek(FILE *stream, long offset, int fromwhere);
off_t lseek (int fd, off_t offset, int whence);

fseek 函数和 lseek 函数类似,但 lseek 返回的是一个 off_t 数值,而 fseek 返回的是一个整型。一个打开的是文件流指针、一个打开的是文件描述符

6、off_t 类型

查看:/usr/include/i386-linux-gnu/sys/types.h 

#ifndef __off_t_defined
# ifndef __USE_FILE_OFFSET64
typedef __off_t off_t;
# else
typedef __off64_t off_t;
# endif
# define __off_t_defined
#endif
如果定义了__USE_FILE_OFFSET64 就把 off_t 定义为 __off64_t ,否则定义为 32 位。

使用 gdb 查看:

编译gdb文件
# gcc -g test.c

# gdb a.out 
GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>...
Reading symbols from /home/tarena/project/c_test/a.out...done.
(gdb) ptype off_t
type = long int

所以,linux中的 off_t 类型默认是 32 位的 long int。

四、函数 read

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

1、参数解析

第一个参数:文件描述符
第二个参数:缓冲区的首地址
第三个参数:读取的数据大小

2、返回值

成功返回读取到的数据大小,若已到文件尾,返回 0,失败返回 -1。

很多种情况可使实际读到的字节数少于要求读的字节数:
(1)读普通文件时,在读到要求字节数之前已经到达了文件尾端。
(2)当从终端设备读时,通常一次最多读一行。
(3)当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
(4)当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
(5)当从某些面向记录的设别(如磁带)读时,一次最多返回一个记录。
(6)当一信号造成中断,而已经读了部分数据量时。

读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。

3、函数功能

表示从指定的文件中读取指定的数据。

4、示例说明

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

int main (void)
{
	char buffer[100];
	int fd = open("a.txt", O_RDWR);
	lseek (fd, 2L, SEEK_SET);
	int size = read (fd, buffer, sizeof (buffer));
	printf ("buffer = %ssize = %d\n", buffer, size);
	close (fd);
	return 0;
}
输出结果:
buffer = CDEFJHIJKLMN
size = 13
说明:读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。

五、函数 write

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

1、参数解析

第一个参数:文件描述符
第二个参数:缓冲区首地址
第三个参数:数据的大小

2、返回值

成功返回写入的数据大小,失败返回 -1。
其返回值通常与 count 的值相同,否则表示出错。write 出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。

3、函数功能

表示向指定的文件中写入指定的数据

4、示例说明

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <string.h>

int main (void)
{
	//int fd = open ("a.txt", O_RDWR | O_APPEND);
	int fd = open ("a.txt", O_RDWR);
	if (-1 == fd)
		perror ("open"), exit (-1);

	lseek (fd, 3, SEEK_SET);
	
	char buffer[5] = "1234";
	int size = write(fd,buffer,strlen(buffer));
	printf ("size = %d\n", size);
	close (fd);

	return 0;
}
输出结果:
size = 4

查看 a.txt
# cat a.txt 
ABC1234HIJKLMN
说明:对于普通文件,写操作从文件的当前偏移量处开始。如果在打开文件时,指定了O_APPEND选项,则在每次写操作之前, 将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。

六、I/O的效率

UNIX环境高级编程书上示例位置在 apue.3e/figlinks 比如本例的 fig3.5
#include "apue.h"

#define	BUFFSIZE	4096

int
main(void)
{
	int		n;
	char	buf[BUFFSIZE];

	while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
		if (write(STDOUT_FILENO, buf, n) != n)
			err_sys("write error");

	if (n < 0)
		err_sys("read error");

	exit(0);
}
重定向操作:标准输入
# ./a.out < 1.txt 
12345678

重定向操作:标准输出
# ./a.out > 2.txt 
ABCDEF
^C
查看2.txt:
# cat 2.txt 
ABCDEF
上篇讲文件描述符时讲过, POSIX 定义了  STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 来代替 0、1、2 这三个符号常量的定义位于头文件  <unistd.h>
查看 /usr/include/unistd.h  
/* Standard file descriptors.  */  
#define STDIN_FILENO    0   /* Standard input.  */  
#define STDOUT_FILENO   1   /* Standard output.  */  
#define STDERR_FILENO   2   /* Standard error output.  */  

1、问题来了,STDIN_FILENO与 stdin的区别?

(1)数据类型不一致

stdin 等类型为 FILE *
STDIN_FILENO 等类型为 int
使用 stdin 的函数主要有:fread、fwrite、fclose等,基本上都以 f 开头
使用 STDIN_FILENO 的函数有:read、write、close等

(2)层次不一致

stdin 等属于标准I/O,高级的输入输出函数。在<stdio.h>。
STDIN_FILENO 等是文件描述符,是非负整数,一般定义为 0, 1, 2,直接调用系统调用,在<unistd.h>。
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
stdin等属于标准库处理的输入流,其声明为 FILE 型的,对应的函数前面都有f开头,如fopen/fread/fwrite/fclose 标准库调用等
STDIN_FILENO等属于系统API接口库,其声明为 int 型,是一个打开文件句柄,对应的函数主要包括 open/read/write/close 等系统级调用。

2、两者关系

对于 stdin 等可以使用 fileno() 函数(用来取得参数 stream 指定的文件流所使用的文件描述符)来取得该文件流对应的文件描述符。
fileno(stdin) = STDIN_FILENO = 0
fileno(stdout) = STDOUT_FILENO = 1
fileno(stderr) = STDERR_FILENO = 2
例如:
#include "apue.h"

#define	BUFFSIZE	4096

int
main(void)
{
	int		n;
	char	buf[BUFFSIZE];

	while ((n = read(fileno(stdin), buf, BUFFSIZE)) > 0)
		if (write(fileno(stdout), buf, n) != n)
			err_sys("write error");

	if (n < 0)
		err_sys("read error");

	exit(0);
}

七、文件共享

UNIX系统支持在不同进程间共享打开文件。
内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是: 
    文件描述符标志; 
    指向一个文件表项的指针。
2)内核为所有打开文件维持一张文件表。每个文件表项包含: 
    文件状态标志(读、写、添写、同步和非阻塞等); 
    当前文件偏移量; 
    指向该文件v节点表项的指针。
3)每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。如i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。 
例如:一个进程有两个不同的打开文件:一个文件从标准输入打开(文件描述符为 0),另一个从标准输出打开(文件描述符为 1)。
#include "apue.h"  
  
#define BUFFSIZE    4096  
  
int  
main(void)  
{  
    int     n;  
    char    buf[BUFFSIZE];  
  
    while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)  
        if (write(STDOUT_FILENO, buf, n) != n)  
            err_sys("write error");  
  
    if (n < 0)  
        err_sys("read error");  
  
    exit(0);  
}  
执行:
# ./a.out < 1.txt > 2.txt 
# cat 1.txt 
12345678
# cat 2.txt 
12345678
该进程对应的 3 张表之间的关系:

再如:两个进程各自打开了同一个文件, 这部分参看:Linux中的文件描述符与打开文件之间的关系
对于以上数据结构,其相关操作的说明如下:
在完成每个 write 后,在文件表项中的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将 i 节点表项中的当前文件长度设置为当前文件偏移量(即该文件增加了);
如果用 O_APPEND 标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表现中的当前文件偏移量首先会被设置为 i 节点表现中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处;
若一个文件用 lseek 定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为 i 节点表项中的当前文件长度;
lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。

八、函数 dup 和 dup2

1、函数 dup

#include <unistd.h>
int dup(int oldfd);
函数功能:
表示根据参数指定的文件描述符进行拷贝,
返回值:
成功返回新的文件描述符,失败返回 -1。
注意:
文件描述符的复制本质上就是让多个文件描述符对应同一个文件表,也就是对应同一个文件。
由 dup 返回的心的文件描述符一定是当前可用文件描述符中的最小数值。
示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main (void)
{
	int newfd = dup (STDOUT_FILENO);
	if (-1 == newfd)
		perror ("Fail to dup"), exit (-1);

	printf ("newfd = %d\n", newfd);
	write (newfd, "hello world\n", 12);
	return 0;
}
输出结果:
newfd = 3
hello world

2、函数 dup2

#include <unistd.h>
int dup2(int oldfd, int newfd);
函数功能:
表示将 newfd 作为参数 oldfd 的拷贝,如果 newfd 已经打开,则先将其关闭;如若 oldfd 等于 newfd,则 dup2 返回 newfd,而不关闭它。否则,newfd 的 FD_CLOEXEC 文件描述符标志被清除,这样 newfd 在进程调用 exec 时是打开状态。
返回值:
成功返回新描述符,也就是 newfd,失败返回 -1。
示例:
//dup/dup2函数的使用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
	//1.打开/创建一个文件
	int fd = open("d.txt",O_RDWR|O_CREAT/*|O_EXCL*/,0644);
	if(-1 == fd)
	{
		perror("open"),exit(-1);
	}
	printf("fd = %d\n",fd);//3
	//2.使用dup函数复制文件描述符
	// int fd2 = fd;
	int fd2 = dup(fd);
	if(-1 == fd2)
	{
		perror("dup"),exit(-1);
	}
	printf("fd2 = %d\n",fd2);//4
	//3.针对不同的描述符进行处理
	write(fd,"A",1);
	write(fd2,"a",1);

	//打开/创建一个新文件
	int fd3 = open("e.txt",O_RDWR|O_CREAT,0644);
	if(-1 == fd3)
	{
		perror("open"),exit(-1);
	}
	printf("fd3 = %d\n",fd3);//5
	
	//使用dup2函数进行描述符的拷贝
	// fd 到 fd3的拷贝 fd3 和 fd4相等
	int fd4 = dup2(fd,fd3);
	printf("fd3 = %d,fd4 = %d\n",fd3,fd4);// 5 5 

	write(fd3,"1",1); //d.txt
	write(fd4,"2",1); //d.txt
	write(fd,"3",1); //d.txt

	//4.关闭所有描述符
	close(fd);
	close(fd2);
	close(fd3);
	return 0;
}

输出结果:
fd = 3
fd2 = 4
fd3 = 5
fd3 = 5,fd4 = 5

查看:d.txt 和 e.txt
# cat d.txt 
Aa123
# cat e.txt 
为空

od 命令查看
# od -c d.txt 
0000000   A   a   1   2   3
0000005
扩展:使用 od 命令观察文件的实际内容。命令行中的 -c 标志表示以字符方式打印文件内容
参看:od 命令

九、函数 fcntl 

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

1、参数解析

第一个参数:文件描述符
第二个参数:操作的命令
    F_DUPFD/F_DUPFD_CLOEXEC  复制文件描述符的功能,寻找最小的有效的大于等于第三个参数 arg 的描述符作为新的描述符。
        与 dup2 函数有所不同的是,不会强制关闭已经被占用的描述符。
    F_GETFD/F_SETFD  获取/设置文件描述符的标志
    F_GETFL/F_SETFL  获取/设置文件状态的标志
   F_GETOWN/F_SETOWN  获取/设置异步 I/O 所有权
    F_SETLK/F_GETLK  加锁/解锁/测试锁是否存在
第三个参数:可变长参数
参数是否需要,主要取决于参数 cmd

2、返回值

F_DUPFD  成功返回新的文件描述符
F_GETFD  成功返回文件描述符的标志值
F_GETFL  成功返回文件状态的标志值
F_GETOWN  成功返回异步 I/O 所有权
其他操作成功返回 0, 所有的操作失败返回 -1

3、函数功能

主要表示根据文件描述符对文件执行的操作
(1)复制文件描述符
(2)获取/设置文件描述符的标志
(3)获取/设置文件状态的标志
(4)获取/设置异步 I/O 所有权
(5)实现文件锁的功能

4、功能讲解

(1)F_DUPFD 

复制文件描述符 fd。新的文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第 3 参数值中各值的最小值。新描述符与 fd 共享同一文件表项。但是,新描述符有它自己的一套文件描述符,其中 FD_CLOEXEC 文件描述符标志被消除(这表示该描述符在 exec 时仍保持有效)。

实际上:调用 dup (fd);  等效于 fcntl (fd, F_DUPFD, 0);
调用 dup2 (fd, fd2);  等效于 close (fd2);  fcntl (fd, F_DUPFD, fd2);
在后一种情况下,dup2 并不完全等同于 close 加上 fcntl。它们之间的区别具体如下:
--dup2 是一个原子操作,而 close 和 fcntl 包括两个函数调用。有可能在 close 和 fcntl 之间调用了信号捕获函数,它可能修改文件描述符。如果不同的线程改变了文件描述符的话也会出现相同的问题。
--dup2 和 fcntl 有不同的 errno。
示例说明: 
//示例一
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

int main (void)
{
	int fd = fcntl (STDOUT_FILENO, F_DUPFD, 0);
	if (-1 == fd)
		perror ("Fail to fcntl"), exit (-1);

	printf ("fd = %d\n", fd);
	write (fd, "hell world\n", 12);
	return 0;
}
输出结果:
fd = 3
hell world
//示例二
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
	//1.打开/创建一个文件
	int fd = open("d.txt",O_RDWR|O_CREAT/*|O_EXCL*/,0644);
	if(-1 == fd)
	{
		perror("open"),exit(-1);
	}
	printf("fd = %d\n",fd);//3
	//2.使用dup函数复制文件描述符
	// int fd2 = fd;
	int fd2 = dup(fd);
	if(-1 == fd2)
	{
		perror("dup"),exit(-1);
	}
	printf("fd2 = %d\n",fd2);//4
	//3.针对不同的描述符进行处理
	write(fd,"A",1);
	write(fd2,"a",1);

	//打开/创建一个新文件
	int fd3 = open("e.txt",O_RDWR|O_CREAT,0644);
	if(-1 == fd3)
	{
		perror("open"),exit(-1);
	}
	printf("fd3 = %d\n",fd3);//5
	
	close (fd3);

	//使用fcntl函数进行描述符的拷贝
	// fd 到 fd3的拷贝 fd3 和 fd4相等
	int fd4 = fcntl (fd, F_DUPFD, fd3);
	printf("fd3 = %d,fd4 = %d\n",fd3,fd4);// 5 5 

	write(fd3,"1",1); //d.txt
	write(fd4,"2",1); //d.txt
	write(fd,"3",1); //d.txt

	//4.关闭所有描述符
	close(fd);
	close(fd2);
	close(fd3);
	return 0;
}
输出结果: 
fd = 3
fd2 = 4
fd3 = 5
fd3 = 5,fd4 = 5

od命令查看: 
# od -c d.txt 
0000000   A   a   1   2   3
0000005

(2)F_DUPFD_CLOEXEC

复制文件描述符,设置与新描述符关联的 FD_CLOEXEC 文件描述符标志的值,返回新文件描述符。

(3)F_GETFD

对应于 fd 的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志 FD_CLOEXEC。
1)上面出现了好几次 FD_CLOEXEC 文件描述符标志,需要对它了解一下:
FD_CLOEXEC 是“文件描述符”的标志,用来设置文件的 close-on-exec 状态。
此标志用来控制在执行 exec 后,是否关闭对应的文件描述符(关闭文件描述符即不能对文件描述符指向的文件进行任何操作)
2)这里又有个新概念 exec :
exec 应该是指 exec系列函数, 在此只了解其中的 execl 函数:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
execl() 其中后缀 "l" 代表 list 也就是参数列表的意思,第一参数path字符指针所指向要执行的文件路径, 接下来的参数代表执行该文件时传递的参数列表:argv[0],argv[1]... 最后一个参数须用空指针NULL作结束
返回值:如果执行成功则函数不会返回, 执行失败则直接返回-1, 失败原因存于errno 中.
函数功能:主要用于实现跳转的功能
示例:
#include <unistd.h>
int main (void)
{
	// 执行/bin目录下的ls, 第一参数为程序名ls, 第二个参数为"-al", 第三个参数为"/etc/passwd"
	execl("/bin/ls", "ls", "-al", "/etc/passwd", NULL);
	return 0;
}
输出结果:
-rw-r--r-- 1 root root 1918 Dec  1 10:28 /etc/passwd
3)FD_CLOEXEC 标志使用
第一、此标志用来控制在执行 exec 后,是否关闭对应的文件描述符
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main (int argc, char *argv[])
{
	if (argc > 1)
	{
		int flags = fcntl (STDOUT_FILENO, F_GETFD);
		if (-1 == flags)
			perror ("fail to F_GETFD"), exit (-1);

		flags |= FD_CLOEXEC;

		if (fcntl (STDOUT_FILENO, F_SETFD, flags) == -1)
			perror ("fail to F_SETFD"), exit (-1);
	}

	execl("/bin/ls", "ls", "-al", "/etc/passwd", NULL);
	return 0;
}
输出结果:
不含有FD_CLOEXEC标志,未关闭对应的文件描述符 STDOUT_FILENO
# ./a.out 
-rw-r--r-- 1 root root 1918 Dec  1 10:28 /etc/passwd

含有FD_CLOEXEC标志,关闭了对应的文件描述符 STDOUT_FILENO 
# ./a.out 1
ls: 写入错误: 错误的文件描述符
第二、在使用fork调用的子进程中,此描述符并不关闭,仍可使用。(验证失败)
//vfork函数和execl函数的使用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

int main (int argc, char *argv[])
{
	if (argc > 1)
	{
		int flags = fcntl (STDOUT_FILENO, F_GETFD);
		if (-1 == flags)
			perror ("fail to F_GETFD"), exit (-1);

		flags |= FD_CLOEXEC;

		if (fcntl (STDOUT_FILENO, F_SETFD, flags) == -1)
			perror ("fail to F_SETFD"), exit (-1);
	}

	//1.使用vfork函数创建子进程
	pid_t pid = vfork();
	if(-1 == pid)
		perror("vfork"),exit(-1);

	//2.子进程调用execl函数跳转
	if(0 == pid) //子进程
	{
		printf("子进程%d开始运行\n",getpid());
		sleep(2);

		//调用execl函数进行跳转
		execl("/bin/ls", "ls", "-al", "/etc/passwd", NULL);

		//执行不到
		printf("子进程结束\n");
	}

	sleep (2);
	//3.父进程执行
	printf("父进程%d开始执行\n",getpid());
	printf("父进程结束\n");
	return 0;
}
不含有FD_CLOEXEC标志,未关闭对应的文件描述符 STDOUT_FILENO
# ./a.out 
子进程3488开始运行
-rw-r--r-- 1 root root 1918 Dec  1 10:28 /etc/passwd
父进程3487开始执行
父进程结束

含有FD_CLOEXEC标志,关闭了对应的文件描述符 STDOUT_FILENO 
# ./a.out 1
子进程3550开始运行
ls: 写入错误: 错误的文件描述符
父进程3549开始执行
父进程结束
4)总结:
FD_CLOEXEC 用来设置文件的 close-on-exec 状态标准。在 exec() 调用后,close-on-exec 标志为0的情况,此文件不被关闭。非零则在 exec() 后被关闭。默认close-on-exec状态为 0,需要通过FD_CLOEXEC设置
看见有这句话,“在使用fork调用的子进程中,此描述符并不关闭,仍可使用。”但是我验证了下,在子进程中也是可以关闭的。 (不过还是不太确认,如果哪位看到这里,可以告诉我一下,这句话是否正确)。

(4)F_SETFD

对于 fd 设置文件描述符标志。新标志值按第 3 个参数(取为整型值)设置。
示例同上。

(5)F_GETFL

对应于 fd 的文件状态标志作为函数值返回。下面是 open 函数时描述的文件状态标志。
文件状态标志说明
O_RDONLY只读打开
O_WRONLY只写打开
O_RDWR读、写打开
O_EXEC只执行打开
O_SEARCH只搜索打开目录
O_APPEND追加写
O_NONBLOCK非阻塞模式
O_SYNC等待写完成(数据和属性)
O_DSYNC等待写完成(仅数据)
O_RSYNC同步读和写
O_FSYNC等待写完成(仅FreeBSD和Mac OS X)
O_ASYNC异步I/O(仅FreeBSD和Mac OS X)
不过,5 个访问方式标志(O_RDONLY、O_WRONLY、O_RDWR、O_EXEC以及O_SEARCH)并不各占1位。因此首先必须用屏蔽字 O_ACCMODE 取得访问方式位,然后将结果与这5个值的每一个相比较。
通过下面的宏定义可以看出:
O_RDONLY、O_WRONLY、O_RDWR这3种标志的值各是0 , 1和2,由于历史原因,这 5 种值互斥 , 一个文件的访问只能有这 5 个值之一
示例说明:
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
  
int main (void)  
{  
    //1.打开/创建一个文件  
    int fd = open ("a.txt", O_WRONLY);  
    if (-1 == fd)  
        perror("open"),exit(-1);  
	
	int flags = fcntl (fd, F_GETFL, 0);
	printf ("flags = %d\n", flags);

    close(fd);  
    return 0;  
}  
输出结果:
flags = 1
对应的函数值查看/usr/include/asm-generic/fcntl.h 
最前面有 0 可以看出数值为八进制,打印转化为十进制, 扩展:进制转换器
#define O_ACCMODE	00000003
#define O_RDONLY	00000000
#define O_WRONLY	00000001
#define O_RDWR		00000002
#ifndef O_CREAT
#define O_CREAT		00000100	/* not fcntl */
#endif
#ifndef O_EXCL
#define O_EXCL		00000200	/* not fcntl */
#endif
#ifndef O_NOCTTY
#define O_NOCTTY	00000400	/* not fcntl */
#endif
#ifndef O_TRUNC
#define O_TRUNC		00001000	/* not fcntl */
#endif
#ifndef O_APPEND
#define O_APPEND	00002000
#endif
#ifndef O_NONBLOCK
#define O_NONBLOCK	00004000
#endif
#ifndef O_DSYNC
#define O_DSYNC		00010000	/* used to be O_SYNC, see below */
#endif
后面还有一个更好玩的,现在讲的功能都可用数字表示的:
#define F_DUPFD		0	/* dup */
#define F_GETFD		1	/* get close_on_exec */
#define F_SETFD		2	/* set/clear close_on_exec */
#define F_GETFL		3	/* get file->f_flags */
#define F_SETFL		4	/* set file->f_flags */
#ifndef F_GETLK
#define F_GETLK		5
#define F_SETLK		6
#define F_SETLKW	7
#endif
#ifndef F_SETOWN
#define F_SETOWN	8	/* for sockets. */
#define F_GETOWN	9	/* for sockets. */
#endif
#ifndef F_SETSIG
#define F_SETSIG	10	/* for sockets. */
#define F_GETSIG	11	/* for sockets. */
#endif

(6)F_SETFL

将文件状态标志设置为第 3 个参数的值(取为整型值)。可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC和 O_ASYNC。
以 O_NONBLOCK 为例:
//示例一
#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
int main (void)    
{    
    //1.打开/创建一个文件    
    int fd = open ("a.txt", O_RDONLY | O_NONBLOCK);    
    if (-1 == fd)    
        perror("open"),exit(-1);    
      
    int flags = fcntl (fd, F_GETFL, 0);  
    printf ("flags = %d\n", flags);  
  //将其设为非阻塞  
	flags &= ~O_NONBLOCK;
	fcntl (fd, F_SETFL, flags);

    int flags1 = fcntl (fd, F_GETFL, 0);  
    printf ("flags1 = %d\n", flags1);  
  
    close(fd);    
    return 0;    
}     
输出结果:
flags = 2048
flags1 = 0
//示例二
#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
    
int main (void)    
{    
    //1.打开/创建一个文件    
    int fd = open ("a.txt", O_RDONLY);    
    if (-1 == fd)    
        perror("open"),exit(-1);    
      
    int flags = fcntl (fd, F_GETFL, 0);  
    printf ("flags = %d\n", flags);  
	flags |= O_NONBLOCK;
	fcntl (fd, F_SETFL, flags);

    int flags1 = fcntl (fd, F_GETFL, 0);  
    printf ("flags1 = %d\n", flags1);  
  
    close(fd);    
    return 0;    
}    
输出结果:
flags = 0
flags1 = 2048
示例说明:
示例一:取消文件的某个flags,比如文件是非阻塞的,想设置成为阻塞:
flags = fcntl(fd,F_GETFL,0);
flags &= ~O_NONBLOCK;
fcntl(fd,F_SETFL,flags);
示例二:增加文件的某个flags,比如文件是阻塞的,想设置成非阻塞:
flags = fcntl(fd,F_GETFL,0);
flags |= O_NONBLOCK;
fcntl(fd,F_SETFL,flags);

(7)F_GETOWN

获取当前接收 SIGIO 和 SIGURG 信号的进程 ID 或进程组 ID。(示例以后写)

(8)F_SETOWN

设置接收 SIGIO 和 SIGURG 信号的进程 ID 或进程组 IF。正的 arg 指定一个进程 ID,负的 arg 表示等于 arg 绝对值的一个进程组 ID。

十、总结

文件I/O这一章讲完,不过有几处没有搞懂和没有讲到的地方。
首先,进程表、文件表、v节点表这部分没有搞明白;原子操作没有讲到;函数 sync、fsync和fdatasync没有讲;函数 fcntl 文件锁部分没有讲;函数 ioctl没有讲;/dev/fd 没有讲。
这些在以后的总结中会涉及到,到时再详细讲吧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聚优致成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值