Linux中的文件IO

1.什么是文件IO
(1)IO就是input/output,输入/输出。文件IO的意思就是读写文件。
2.linux常用文件IO接口
(1)open、close、write、read、lseek
3.文件操作的一般步骤
(1)一般先open打开文件,得到文件描述符,然后对文件进行读写(其他)操作,最后close关闭文件即可
(2)如果文件存在块设备中的文件系统,称为静态文件。当open打开一个文件时,linux内核做的操作包括内核在进程中建立了一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中特定地址管理存放(叫动态文件)。
(3)打开文件对这个文件的读写操作,都是针对内存中这份动态文件,而不是静态文件。此时动态文件和静态文件不同步,需要close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新到块设备中的静态文件。
4.为什么这么设计读写文件
(1)块设备本身有读写限制,对块设备进行操作非常不灵活,而内存可以按字节为单位来操作,而且可以随机操作,灵活。
5.文件描述符
(1)文件描述符实质是一个数字,这个数组在一个进程中表示一个特定的含义,当我们open打开一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护这个动态文件的这些数据结构挂钩绑定上了,以后我们应用程序如果要操作这一个动态文件,只需要用这个文件描述符进行区分
(2)作用域:当前进程。
(3)文件描述符用来区分一个程序打开的多个文件。
(4)文件描述符的合法范围是0或者一个正数,不可能是负数。
6.实时查man手册
(1)man 1xxx 查linux shell命令,man 2 xxx 查API,man 3 xxx 查库函数

7.读取文件内容
ssize_t read(int fd, void* buf, size_t count);
d表示要读取哪个文件,fd一般由前面的open返回得到
buf是应用程序自己提供的一段内存缓冲区,用来存储读出的内容
count是我们要读取的字节数
返回值ssize_t类型是linux内核用typedef重定义的一个类型(其实就是int),返回值表示成功读取的字节数。如果在调用read之前到达文件末尾,则返回0。

8.向文件文件写入
ssize_t write(int fd, const void *buf, size_t count);
fd:文件描述符
buf:通常一个字符串,需要写入的字符串
count:每次写入的字节数
返回值:
成功:返回写入的字节数
失败:返回-1并设置为errno

9.open函数flag详解1
int open(const char *pathname, int flags);
(1)linux中文件有读写权限,当我们在open打开文件也需要附带权限说明。O_RDONLY(只读方式打开),O_WRONLY(只写方式打开),O_RDWR(可读可写方式打开)
(2)O_TRUNC属性去打开文件时,如果这个文件中本来是有内容的,则原来的内容会被丢弃。
(3)O_APPEND属性去打开文件时,如果这个文件中本来是有内容的,则新写入的内容会接续到原来内容的后面。
(4)默认不使用O_APPEND和O_TRUNC属性时,不读不写的时候,原来的文件中的内容保持不变。

10.open函数flag详解2
(1)O_CREAT,为了打开一个并不存在的文件,当前文件不存在则会创建并打开它。
(2)问题:我们本来是想去创建一个新文件的,但是把文件名搞错了弄成了一个老文件名,结果老文件就被意外修改了。我们希望的效果是:如果我CREAT要创建的是一个已经存在的名字的文件,则给我报错,不要去创建。
解决方案:O_EXCL标志和O_CREAT标志来结合使用,当这连个标志一起的时候,则没有文件时创建文件,有这个文件时会报错提醒我们。
(3)open函数在使用O_CREAT标志去创建文件时,可以使用第三个参数mode来指定要创建的文件的权限。mode使用4个数字来指定权限的,其中后面三个很重要,对应我们要创建的这个文件的权限标志。譬如一般创建一个可读可写不可执行的文件就用0666

11.O_NONBLOCK
(1)阻塞与非阻塞。如果一个函数是阻塞式的,则我们调用这个函数时当前进程有可能被卡住(阻塞住,实质是这个函数内部要完成的事情条件不具备,当前没法做,要等待条件成熟),函数被阻塞住了就不能立刻返回;如果一个函数是非阻塞式的那么我们调用这个函数后一定会立即返回,但是函数有没有完成任务不一定。
(2)阻塞和非阻塞是两种不同的设计思路,并没有好坏。总的来说,阻塞式的结果有保障但是时间没保障;非阻塞式的时间有保障但是结果没保障。
(3)打开一个文件默认是阻塞式,如果希望是非阻塞式打开文件,则flag中要加O_NONBLOCK
(4)只用于设备文件,而不用于普通文件。

12.O_SYNC
(1)write阻塞等待底层完成写入才返回到应用层。
(2)无O_SYNC时write只是将内容写入底层缓冲区即可返回,然后底层(操作系统中负责实现open、write这些操作的那些代码,也包含OS中读写硬盘等底层硬件的代码)在合适的时候会将buf中的内容一次性的同步到硬盘中。这种设计是为了提升硬件操作的性能和销量,提升硬件寿命;但是有时候我们希望硬件不用等待,直接将我们的内容写入硬盘中,这时候就可以用O_SYNC标志。

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



int main(int argc, char *argv[])
{
	int fd = -1;		// fd 就是file descriptor,文件描述符
	char buf[100] = {0};
	char writebuf[20] = "l love linux";
	int ret = -1;
	
	// 第一步:打开文件
	fd = open("a.txt", O_RDWR);
	if (-1 == fd)		// 有时候也写成: (fd < 0)
	{
		printf("文件打开错误\n");
	}
	else
	{
		printf("文件打开成功,fd = %d.\n", fd);
	}
	
	// 第二步:读写文件
	// 写文件
	ret = write(fd, writebuf, strlen(writebuf));
	if (ret < 0)
	{
		printf("write失败.\n");
	}
	else
	{
		printf("write成功,写入了%d个字符\n", ret);
	}
/*	
	// 读文件
	ret = read(fd, buf, 5);
	if (ret < 0)
	{
		printf("read失败\n");
	}
	else
	{
		printf("实际读取了%d字节.\n", ret);
		printf("文件内容是:[%s].\n", buf);
	}
*/	
	// 第三步:关闭文件
	close(fd);
	
	return 0;
}

在这里插入图片描述

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



int main(int argc, char *argv[])
{
	int fd = -1;		// fd 就是file descriptor,文件描述符
	char buf[100] = {0};
	char writebuf[20] = "l love linux";
	int ret = -1;
	
	// 第一步:打开文件
	fd = open("a.txt", O_RDWR | O_APPEND | O_TRUNC);
	if (-1 == fd)		// 有时候也写成: (fd < 0)
	{
		printf("文件打开错误\n");
		// return -1;
		_exit(-1);
	}
	else
	{
		printf("文件打开成功,fd = %d.\n", fd);
	}

#if 1	
	// 第二步:读写文件
	// 写文件
	ret = write(fd, writebuf, strlen(writebuf));
	if (ret < 0)
	{
		printf("write失败.\n");
		_exit(-1);
	}
	else
	{
		printf("write成功,写入了%d个字符\n", ret);
	}
#endif


#if 0
	// 读文件
	ret = read(fd, buf, 5);
	if (ret < 0)
	{
		printf("read失败\n");
		_exit(-1);
	}
	else
	{
		printf("实际读取了%d字节.\n", ret);
		printf("文件内容是:[%s].\n", buf);
	}
#endif	

	// 第三步:关闭文件
	close(fd);
	
	_exit(0);
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>



int main(int argc, char *argv[])
{
	int fd = -1;		// fd 就是file descriptor,文件描述符
	char buf[100] = {0};
	char writebuf[20] = "l love linux";
	int ret = -1;
	
	// 第一步:打开文件
	//fd = open("a.txt", O_RDWR | O_CREAT | O_EXCL, 0666);
	fd = open("a.txt", O_RDONLY);
	if (-1 == fd)		// 有时候也写成: (fd < 0)
	{
		//printf("\n");
		perror("文件打开错误");
		// return -1;
		_exit(-1);
	}
	else
	{
		printf("文件打开成功,fd = %d.\n", fd);
	}

#if 1	
	// 第二步:读写文件
	// 写文件
	ret = write(fd, writebuf, strlen(writebuf));
	if (ret < 0)
	{
		//printf("write失败.\n");
		perror("write失败");
		_exit(-1);
	}
	else
	{
		printf("write成功,写入了%d个字符\n", ret);
	}
#endif


#if 0
	// 读文件
	ret = read(fd, buf, 5);
	if (ret < 0)
	{
		printf("read失败\n");
		_exit(-1);
	}
	else
	{
		printf("实际读取了%d字节.\n", ret);
		printf("文件内容是:[%s].\n", buf);
	}
#endif	

	// 第三步:关闭文件
	close(fd);
	
	_exit(0);
}

12.exit、_exit、_Exit退出进程
我们如何退出程序?
第一种;在main用return,一般原则是程序正常终止return 0,如果程序异常终止则return -1。
第一种:正式终止进程(程序)应该使用exit或者_exit或者_Exit之一。
在这里插入图片描述从图中可以看出,_exit 函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核的各种数据结构;exit 函数则在这些基础上做了一些小动作,在执行退出之前还加了若干道工序。exit() 函数与 _exit() 函数的最大区别在于exit()函数在调用exit 系统调用前要检查文件的打开情况,把文件缓冲区中的内容写回文件。也就是图中的“清理I/O缓冲”。
exit: void exit(int status)
_exit: void _exit(int status)
函数传入值:status 是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其他的数值表示出现了错误,进程非正常结束。在实际编程时,父进程可以利用wait 系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理。
传入的参数是程序退出时的状态码,0表示正常退出,其他表示非正常退出,一般都用-1或者1
printf(const char *fmt,…)函数使用的是缓冲I/O方式,该函数在遇到 “\n” 换行符时自动从缓冲区中将记录读出。

13.文件读写的一些细节
errno和perror
(1)errno就是error number,意思就是错误号码。linux系统对错误做了编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉这个函数哪里错了。
(2)linux系统本身提供了一个函数perror(printf error),perror函数内部会读取errno并且将这个不好认的数字直接给转成对应的错误信息字符串,然后print打印出来。

14.read和write的count
(1)count和返回值的关系。count表示想要读取或写的字节数,返回值表示实际完成读取或写的字节数。函数返回值有可能小于count。
(2)count和阻塞非阻塞结合起来。
(3)如果要读取或者写入一个很庞大的文件,不可能一次把count设置完,应该设置为一个合适的大小,分开读取。

15.文件io效率和标准IO
(1)文件IO就指的是我们当前在讲的open、close、write、read等API函数构成的一套用来读写文件的体系,这套体系可以很好的完成文件读写,但是效率并不是最高的。
(2)应用层C语言库函数提供了一些用来做文件读写的函数列表,叫标准IO。标准IO由一系列的C库函数构成(fopen、fclose、fwrite、fread),这些标准IO函数其实是由文件IO封装而来的(fopen内部其实调用的还是open,fwrite内部还是通过write来完成文件写入的)。标准IO加了封装之后主要是为了在应用层添加一个缓冲机制,这样我们通过fwrite写入的内容不是直接进入内核中的buf,而是先进入应用层标准IO库自己维护的buf中,然后标准IO库自己根据操作系统单次write的最佳count来选择好的时机来完成write到内核中的buf(内核中的buf再根据硬盘的特性来选择好的实际去最终写入硬盘中)。

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



int main(int argc, char *argv[])
{
	int fd = -1;		// fd 就是file descriptor,文件描述符
	char buf[100] = {0};
	char writebuf[20] = "l love linux";
	int ret = -1;
	
	// 第一步:打开文件
	//fd = open("a.txt", O_RDWR | O_CREAT | O_EXCL, 0666);
	fd = open("a.txt", O_RDONLY);
	if (-1 == fd)		// 有时候也写成: (fd < 0)
	{
		//printf("\n");
		perror("文件打开错误");
		// return -1;
		_exit(-1);
	}
	else
	{
		printf("文件打开成功,fd = %d.\n", fd);
	}

#if 1	
	// 第二步:读写文件
	// 写文件
	ret = write(fd, writebuf, strlen(writebuf));
	if (ret < 0)
	{
		//printf("write失败.\n");
		perror("write失败");
		_exit(-1);
	}
	else
	{
		printf("write成功,写入了%d个字符\n", ret);
	}
#endif


#if 0
	// 读文件
	ret = read(fd, buf, 5);
	if (ret < 0)
	{
		printf("read失败\n");
		_exit(-1);
	}
	else
	{
		printf("实际读取了%d字节.\n", ret);
		printf("文件内容是:[%s].\n", buf);
	}
#endif	

	// 第三步:关闭文件
	close(fd);
	
	_exit(0);
}

16.lseek详解
(1)文件指针:当我们要对一个文件进行读写时,一定需要先打开这个文件,动态文件在内存中的形态就是文件流的形式。
(2)在动态文件中,我们会通过文件指针来表征这个正在操作的位置。所谓文件指针,就是我们文件管理表这个结构体里面的一个指针。所以文件指针其实是vnode中的一个元素。这个指针表示当前我们正在操作文件流的哪个位置。这个指针不能被直接访问,linux系统用lseek函数来访问这个文件指针。
(3)打开一个空文件时,默认文件指针指向文件流的开始。

17.lseek计算文件长度
off_t lseek(int fd, off_t offset, int whence);
参数 whence 为下列其中一种:
SEEK_SET 参数offset 即为新的读写位置.
SEEK_CUR 以目前的读写位置往后增加offset 个位移量.
SEEK_END 将读写位置指向文件尾后再增加offset 个位移量. 当whence 值为SEEK_CUR 或
SEEK_END 时, 参数offet 允许负值的出现.
返回值:当调用成功时则返回目前的读写位置, 也就是距离文件开头多少个字节. 若有错误则返回-1, errno 会存放错误代码.

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



int cal_len(const char *pathname)
{
	int fd = -1;		// fd 就是file descriptor,文件描述符
	int ret = -1;
	
	// 第一步:打开文件
	fd = open(pathname, O_RDONLY);
	if (-1 == fd)		// 有时候也写成: (fd < 0)
	{
		//printf("\n");
		perror("文件打开错误");
		// return -1;
		return -1;
	}
	//else
	//{
		//printf("文件打开成功,fd = %d.\n", fd);
	//}
	
	// 此时文件指针指向文件开头
	// 我们用lseek将文件指针移动到末尾,然后返回值就是文件指针距离文件开头的偏移量,也就是文件的长度了
	ret = lseek(fd, 0, SEEK_END);
	
	return ret;
}



int main(int argc, char *argv[])
{
	int fd = -1;		// fd 就是file descriptor,文件描述符
	int ret = -1;
	
	if (argc != 2)
	{
		printf("usage: %s filename\n", argv[0]);
		_exit(-1);
	}
	
	printf("文件长度是:%d字节\n", cal_len(argv[1]));
	
	return 0;
}

18.用lseek构建空洞文件
(1)使用lseek往后跳过一段,再write写入一段。
ret = lseek(fd, 10, SEEK_SET);

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



int main(int argc, char *argv[])
{
	int fd = -1;		// fd 就是file descriptor,文件描述符
	char buf[100] = {0};
	char writebuf[20] = "abcd";
	int ret = -1;
	
	// 第一步:打开文件
	fd = open("123.txt", O_RDWR | O_CREAT);
	//fd = open("a.txt", O_RDONLY);
	if (-1 == fd)		// 有时候也写成: (fd < 0)
	{
		//printf("\n");
		perror("文件打开错误");
		// return -1;
		_exit(-1);
	}
	else
	{
		printf("文件打开成功,fd = %d.\n", fd);
	}
	
	ret = lseek(fd, 10, SEEK_SET);
	printf("lseek, ret = %d.\n", ret);
	
	


#if 1	
	// 第二步:读写文件
	// 写文件
	ret = write(fd, writebuf, strlen(writebuf));
	if (ret < 0)
	{
		//printf("write失败.\n");
		perror("write失败");
		_exit(-1);
	}
	else
	{
		printf("write成功,写入了%d个字符\n", ret);
	}
#endif


#if 1
	// 读文件
	ret = read(fd, buf, 20);
	if (ret < 0)
	{
		printf("read失败\n");
		_exit(-1);
	}
	else
	{
		printf("实际读取了%d字节.\n", ret);
		printf("文件内容是:[%s].\n", buf);
	}
#endif	

	// 第三步:关闭文件
	close(fd);
	
	_exit(0);
}

19.多次打开同一文件与O_APPEND
(1)重复打开同一文件读取
一个进程中两次打开同一文件,结果是fd1和fd2分别读。
原因:使用open两次打开同一文件,fd1和fd2所对应的文件指针是不同的2两个独立的指针。文件指针是包含在动态文件的文件管理表中的,所以linux系统的进程中不同的fd对应不同的独立的文件管理表。
(2).重复打开同一文件写入
默认情况下,一个进程两次打开同一文件,结果是fd1和fd2分别写。
(3)加O_APPEND解决覆盖问题
如果希望接续写而不是分别写,方法是在open时加O_APPEND标志即可。
(4)O_APPEND实现原理和其原子操作性说明
1.O_APPEND为什么能够将分别写改为接续写?关键的核心的东西是文件指针。分别写的内部原理就是2个fd拥有不同的文件指针,并且彼此只考虑自己的位移。但是O_APPEND标志可以让write和read函数内部多做一件事情,就是移动自己的文件指针的同时也去把别人的文件指针同时移动。(也就是说即使加了O_APPEND,fd1和fd2还是各自拥有一个独立的文件指针,但是这两个文件指针关联起来了,一个动了会通知另一个跟着动)
2.O_APPEND对文件指针的影响,对文件的读写是原子的。
3.原子操作的含义是:整个操作一旦开始是不会被打断的,必须直到操作结束其他代码才能得以调度运行,这就叫原子操作。每种操作系统中都有一些机制来实现原子操作,以保证那些需要原子操作的任务可以运行。

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



int main(int argc, char *argv[])
{
	int fd1 = -1, fd2 = -1;		// fd 就是file descriptor,文件描述符
	char buf[100] = {0};
	char writebuf[20] = "l love linux";
	int ret = -1;
	
	// 第一步:打开文件
	fd1 = open("a.txt", O_RDWR);
	fd2 = open("a.txt", O_RDWR);
	
	//fd = open("a.txt", O_RDONLY);
	if ((-1 == fd1) || (fd2 == -1))		// 有时候也写成: (fd < 0)
	{
		//printf("\n");
		perror("文件打开错误");
		// return -1;
		_exit(-1);
	}
	else
	{
		printf("文件打开成功,fd1 = %d. fd2 = %d.\n", fd1, fd2);
	}
	


#if 0	
	// 第二步:读写文件
	// 写文件
	ret = write(fd, writebuf, strlen(writebuf));
	if (ret < 0)
	{
		//printf("write失败.\n");
		perror("write失败");
		_exit(-1);
	}
	else
	{
		printf("write成功,写入了%d个字符\n", ret);
	}
#endif


#if 1
	while(1)
	{
		// 读文件
		memset(buf, 0, sizeof(buf));
		ret = read(fd1, buf, 2);
		if (ret < 0)
		{
			printf("read失败\n");
			_exit(-1);
		}
		else
		{
			//printf("实际读取了%d字节.\n", ret);
			printf("fd1:[%s].\n", buf);
		}
		
		sleep(1);
		
		// 读文件
		memset(buf, 0, sizeof(buf));
		ret = read(fd2, buf, 2);
		if (ret < 0)
		{
			printf("read失败\n");
			_exit(-1);
		}
		else
		{
			//printf("实际读取了%d字节.\n", ret);
			printf("fd2:[%s].\n", buf);
		}
		
	}

	
#endif	


	// 第三步:关闭文件
	close(fd1);
	close(fd2);
	
	_exit(0);
}

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



int main(int argc, char *argv[])
{
	int fd1 = -1, fd2 = -1;		// fd 就是file descriptor,文件描述符
	char buf[100] = {0};
	char writebuf[20] = "l love linux";
	int ret = -1;
	
	// 第一步:打开文件
	fd1 = open("a.txt", O_RDWR | O_TRUNC | O_CREAT | O_APPEND, 0666);
	fd2 = open("a.txt", O_RDWR | O_TRUNC | O_CREAT | O_APPEND, 0666);
	
	//fd = open("a.txt", O_RDONLY);
	if ((-1 == fd1) || (fd2 == -1))		// 有时候也写成: (fd < 0)
	{
		//printf("\n");
		perror("文件打开错误");
		// return -1;
		_exit(-1);
	}
	else
	{
		printf("文件打开成功,fd1 = %d. fd2 = %d.\n", fd1, fd2);
	}
	


#if 1
	while (1)
	{
		// 第二步:读写文件
		// 写文件
		ret = write(fd1, "ab", 2);
		if (ret < 0)
		{
			//printf("write失败.\n");
			perror("write失败");
			_exit(-1);
		}
		else
		{
			printf("write成功,写入了%d个字符\n", ret);
		}
		
		
		
		ret = write(fd2, "cd", 2);
		if (ret < 0)
		{
			//printf("write失败.\n");
			perror("write失败");
			_exit(-1);
		}
		else
		{
			printf("write成功,写入了%d个字符\n", ret);
		}
		sleep(1);
	}
	
	
	
#endif


#if 0
	while(1)
	{
		// 读文件
		memset(buf, 0, sizeof(buf));
		ret = read(fd1, buf, 2);
		if (ret < 0)
		{
			printf("read失败\n");
			_exit(-1);
		}
		else
		{
			//printf("实际读取了%d字节.\n", ret);
			printf("fd1:[%s].\n", buf);
		}
		
		sleep(1);
		
		// 读文件
		memset(buf, 0, sizeof(buf));
		ret = read(fd2, buf, 2);
		if (ret < 0)
		{
			printf("read失败\n");
			_exit(-1);
		}
		else
		{
			//printf("实际读取了%d字节.\n", ret);
			printf("fd2:[%s].\n", buf);
		}
		
	}

	
#endif	


	// 第三步:关闭文件
	close(fd1);
	close(fd2);
	
	_exit(0);
}

20.文件共享的实现方式
(1)什么是文件共享
文件共享是同一个文件(同一个inode,同一个pathname)被多个独立的读写体(几乎可以理解为多个文件描述符)去同时(一个打开尚未关闭的同时另一个去操作)操作。
(2)意义:可以通过文件共享来实现多线程同时操作同一个大文件,以减少文件读写时间,提升效率。

21.文件共享的3种实现方式
(1)文件共享的核心是多个文件描述符指向同一个文件。
(2)常见的3种文件共享的情况:第一种是同一个进程多次使用open打开同一文件;第二种是在不同进程中去分别open打开同一文件(因为两个fd在不同的进程中,所以两个fd的数字可以相同也可以不同);第三种是linux系统提供了dup和dup2两个API来让进程复制文件描述符。

22.剖析文件描述符
(1)文件描述符的本质是一个数字,这个数字本质上是进程表中文件描述符表的一个表项,进程通过文件描述符作为index去索引查表得到文件表指针,再间接访问得到这个文件对应的文件表。
(2)文件描述符这个数字是open系统调用内部由操作系统自动分配的,操作系统分配这个fd时也不是随意分配,也是遵照一定的规律的,我们现在就要研究这个规律。
(3)操作系统规定,fd从0开始依次增加。fd也是有最大限制的,在linux的早期版本中(0.11)fd最大是20,所以当时一个进程最多允许打开20个文件。linux中文件描述符表是个数组(不是链表),所以这个文件描述符表其实就是一个数组,fd是index,文件表指针是value
(4)当我们去open时,内核会从文件描述符表中挑选一个最小的未被使用的数字给我们返回。也就是说如果之前fd已经占满了0-9,那么我们下次open得到的一定是10.(但是如果上一个fd得到的是9,下一个不一定是10,这是因为可能前面更小的一个fd已经被close释放掉了)
(5)fd中0、1、2已经默认被系统占用了,因此用户进程得到的最小的fd就是3了。
(6)linux内核占用了0、1、2这三个fd是有用的,当我们运行一个程序得到一个进程时,内部就默认已经打开了3个文件,这三个文件对应的fd就是0、1、2。这三个文件分别叫stdin、stdout、stderr。也就是标准输入、标准输出、标准错误。
(7)标准输入一般对应的是键盘(可以理解为:0这个fd对应的是键盘的设备文件),标准输出一般是LCD显示器(可以理解为:1对应LCD的设备文件)
(8)printf函数其实就是默认输出到标准输出stdout上了。stdio中还有一个函数叫fprintf,这个函数就可以指定输出到哪个文件描述符中。

int fprintf(FILE *stream, const char *format, ...)

参数:
stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
format – 这是 C 字符串,包含了要被写入到流 stream 中的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是 %[flags][width][.precision][length]specifier,
返回值
如果成功,则返回写入的字符总数,否则返回一个负数。

23.文件描述符的复制(dup和dup2)
24.使用dup进行文件描述符复制1
(1)dup系统调用对fd进行复制,会返回一个新的文件描述符(譬如原来的fd是3,返回的就是4)
(2)dup系统调用有一个特点,就是自己不能指定复制后得到的fd的数字是多少,而是由操作系统内部自动分配的,分配的原则遵守fd分配的原则。
(3)dup返回的fd和原来的oldfd都指向oldfd打开的那个动态文件,操作这两个fd实际操作的都是oldfd打开的那个文件。实际上构成了文件共享。
(4)dup返回的fd和原来的oldfd同时向一个文件写入时,结果是接续写
缺陷:dup不能指定分配的新的文件描述符的数字,dup2系统调用修复了这个缺陷。
当调用dup函数时,内核在进程中创建一个新的文件描述符,此描述符是当前可用文件描述符的最小数值,这个文件描述符指向oldfd所拥有的文件表项。

练习:通过使用close和dup配合进行文件的重定位

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


#define FILENAME	"1.txt"


int main(void)
{
	int fd1 = -1, fd2 = -1;
	
	fd1 = open(FILENAME, O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (fd1 < 0)
	{
		perror("open");
		return -1;
	}
	printf("fd1 = %d.\n", fd1);
	
	close(1);		// 1就是标准输出stdout
	
	
	// 复制文件描述符
	fd2 = dup(fd1);		// fd2一定等于1,因为前面刚刚关闭了1,这句话就把
	// 1.txt文件和标准输出就绑定起来了,所以以后输出到标准输出的信息就
	// 可以到1.txt中查看了。
	printf("fd2 = %d.\n", fd2);
	printf("this is for test");
	
	close(fd1);
	return -1;
}

25.使用dup2进行文件描述符复制
(1)dup2和dup的作用是一样的,但是dup2允许用户指定新的文件描述符的数字。

 int dup2(int oldfd, int newfd);

dup2与dup区别是dup2可以用参数newfd指定新文件描述符的数值。若参数newfd已经被程序使用,则系统就会将newfd所指的文件关闭,若newfd等于oldfd,则返回newfd,而不关闭newfd所指的文件。dup2所复制的文件描述符与原来的文件描述符共享各种文件状态。共享所有的锁定,读写位置和各项权限或flags等.
(2)练习:dup2共享文件交叉写入测试
(1)dup2复制的文件描述符,和原来的文件描述符虽然数字不一样,但是这连个指向同一个打开的文件
(2)交叉写入的时候,结果是接续写

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


#define FILENAME	"1.txt"


int main(void)
{
	int fd1 = -1, fd2 = -1;
	
	fd1 = open(FILENAME, O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (fd1 < 0)
	{
		perror("open");
		return -1;
	}
	printf("fd1 = %d.\n", fd1);
	
	//close(1);		// 1就是标准输出stdout
	
	
	// 复制文件描述符
	//fd2 = dup(fd1);		// fd2一定等于1,因为前面刚刚关闭了1,这句话就把
	// 1.txt文件和标准输出就绑定起来了,所以以后输出到标准输出的信息就
	// 可以到1.txt中查看了。
	
	fd2 = dup2(fd1, 16);
	printf("fd2 = %d.\n", fd2);
//	printf("this is for test");

	while (1)
	{
		write(fd1, "aa", 2);
		sleep(1);
		write(fd2, "bb", 2);
	}


	
	close(fd1);
	return -1;
}

26.命令行中重定位命令 >
(1)linux中的shell命令执行后,打印结果都是默认进入stdout,(本质上这些命令ls,pwd等都是调用printf进行打印),所以可以在linux的终端shell中直接看到命令执行的结果。
(2)能够让ls、pwd,等命令的输出给重定位到一个文件中,实际上linux终端支持一个重定位的符号>可以完成这个功能。
(3)>的实现原理:利用open+close+dup,open打开一个文件1.txt,然后close关闭stdout,然后dup将1和2.txt文件关联起来即可。
程序如上面23所示.

27.fcntl函数介绍

 int fcntl(int fd, int cmd, ... /* arg */ );

(1)fcntl函数是一个多功能文件管理的工具箱,接收2个参数+1个变参。第一个参数是fd表示要操作哪个文件,第二个参数是cmd表示要进行哪个命令操作。变参是用来传递参数的,要配合cmd来使用。
(2)fcntl的常用cmd:F_DUPFD这个cmd的作用是复制文件描述符(作用类似于dup和dup2),这个命令的功能是从可用的fd数字列表中找一个比arg大或者和arg一样大的数字作为oldfd的一个复制的fd,和dup2有点像但是不同。dup2返回的就是我们指定的那个newfd否则就会出错,但是F_DUPFD命令返回的是>=arg的最小的那一个数字。

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


#define FILENAME	"1.txt"


int main(void)
{
	int fd1 = -1, fd2 = -1;
	
	fd1 = open(FILENAME, O_RDWR | O_CREAT | O_TRUNC, 0644);
	if (fd1 < 0)
	{
		perror("open");
		return -1;
	}
	printf("fd1 = %d.\n", fd1);
	
	close(1);
	
	fd2 = fcntl(fd1, F_DUPFD, 0);
	printf("fd2 = %d.\n", fd2);

	while (1)
	{
		write(fd1, "aa", 2);
		sleep(1);
		write(fd2, "bb", 2);
	}


	
	close(fd1);
	return -1;
}

28.标准IO库介绍
(1)库函数比API还有一个优势就是:API在不同的操作系统之间是不能通用的,但是C库函数在不同操作系统中几乎是一样的。所以C库函数具有可移植性而API不具有可移植性。
(2)性能上和易用性上看,C库函数一般要好一些。譬如IO,文件IO是不带缓存的,而标准IO是带缓存的,因此标准IO比文件IO性能要更高。
(3)简单的标准IO读写文件实例

#include <stdio.h>		// standard input output
#include <stdlib.h>
#include <string.h>


#define FILENAME	"1.txt"

int main(void)
{
	FILE *fp = NULL;
	size_t len = -1;
	//int array[10] = {1, 2, 3, 4, 5};
	char buf[100] = {0};
	
	fp = fopen(FILENAME, "r+");
	if (NULL == fp)
	{
		perror("fopen");
		exit(-1);
	}
	printf("fopen success. fp = %p.\n", fp);
	
	// 在这里去读写文件
	memset(buf, 0, sizeof(buf));
	len = fread(buf, 1, 10, fp);
	printf("len = %d.\n", len);
	printf("buf is: [%s].\n", buf);

#if 0	
	fp = fopen(FILENAME, "w+");
	if (NULL == fp)
	{
		perror("fopen");
		exit(-1);
	}
	printf("fopen success. fp = %p.\n", fp);
	
	// 在这里去读写文件
	//len = fwrite("abcde", 1, 5, fp);
	//len = fwrite(array, sizeof(int), sizeof(array)/sizeof(array[0]), fp);
	len = fwrite(array, 4, 10, fp);
	printf("len = %d.\n", len);
#endif	
	
	fclose(fp);
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值