一、内核态与用户态
现代的CPU都具有不同的操作模式,代表不同的级别,不同的级别具有不同的功能,在较低的级别中将禁止某些操作。Linux系统设计时利用了这种硬件特性,使用了两个级别,最高级别和最低级别,内核运行在最高级别(内核态),这个级别可以进行所有操作,而应用程序运行在较低级别(用户态),在这个级别,处理器控制着对硬件的直接访问以及对内存的非授权访问。内核态和用户态有自己的内存映射,即自己的地址空间。正是有了不同运行状态的划分,才有了上下文的概念。用户空间的应用程序,如果想要请求系统服务,比如操作一个物理设备,或者映射一段设备空间的地址到用户空间,就必须通过系统调用来(操作系统提供给用户空间的接口函数)实现。如下图所示:
文件读写主要牵涉到了如下五个操作:打开、关闭、读、写、定位。在Linux系统中,提供了两套API,一套是C标准API:fopen、fclose、fread、fwrite、fseek,另一套则是POSIX定义的系统API:open、close、read、write、seek。
所谓系统调用是指操作系统提供给用户程序调用的一组“特殊”接口,用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务。例如用户可以通过进程控制相关的系统调用来创建进程、实现进程调度、进程管理等。
在这里,为什么用户程序不能直接访问系统内核提供的服务呢?这是由于在 Linux 中,为了更好地保护内核空间,将程序的运行空间分为内核空间和用户空间(也就是常称的内核态和用户态),它们分别运行在不同的级别上,在逻辑上是相互隔离的。因此,用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间的函数。 但是,在有些情况下,用户空间的进程需要获得一定的系统服务(调用内核空间程序),这时操作系统就必须利用系统提供给用户的“特殊接口”——系统调用规定用户进程进入内核空间的具体位 置。进行系统调用时,程序运行空间需要从用户空间进入内核空间,处理完后再返回到用户空间。
二、对文件操作有两种方式
A、系统调用方式,这个是基于具体的操作系统
B、调用C库函数方式
Linux 系统调用部分是非常精简的系统调用(只有 250 个左右),它继承了 UNIX 系统调用中最基本和最有用的部分。这些系统调用按照功能逻辑大致可分为进程控制、进程间通信、文件系统控制、系统控制、存储管理、网络管理、socket 控制、用户管理等几类。在 Linux 中对目录和设备的操作都等同于文件的操作,因此,大大简化了系统对不同设备的处理,提高了效率。Linux 中的文件主要分为 4 种:普通文件、目录文件、链接文件和设备文件。 那么,内核如何区分和引用特定的文件呢?这里用到的就是一个重要的概念——文件描述符。文件描述符是文件系统中连接用户空间和内核空间的枢纽,当我们打开一个或者创建一个文件时,内核空间会创建相应的结构,并且生成一个整型的变量传递给用户空间的对应进程,而进程则用这个文件描述符来对文件进行操作。要注意的是,文件描述符是一个有限的资源,因此,在使用完毕后要及时释放,一般是调用close()函数来关闭的。在linux系统中有3个已经分配好的文件描述符,那就是标准输入,标准输出和标准错误,他们的文件描述符分别为0,1,2。
IO操作时,操作系统要从用户态转换为内核态的,而这个转换过程相对来说比较慢,因此可以通过缓冲的形式减少转换到内核态的次数。那么,缓冲IO函数又是如何工作的呢?
1. 当用fopen打开文件时,除了分配文件句柄外,还额外申请了一个缓冲区。
2. 读文件时,会首先读到缓冲区中,然后返回用户需要的部分,多余的部分仍然放在缓冲区,下次再读的时候可以直接从缓冲区中返回。
3. 写文件时,会先写到缓冲区中,等缓冲区满后再统一写到文件中。
通常,一个进程启动时,都会打开 3 个文件:标准输入、标准输出和标准出错处理。这3 个文件分别对应文件描述符为 0、1 和 2(也就是宏替换 STDIN_FILENO、STDOUT_FILENO和 STDERR_FILENO)。 基于文件描述符的 I/O 操作虽然不能移植到类 Linux 以外的系统上去(如 Windows),但它往往是实现某些 I/O 操作的惟一途径,如 Linux 中低级文件操作函数、多路 I/O、TCP/IP 套接字编程接口等。同时,它们也很好地兼容 POSIX 标准,因此,可以很方便地移植到任何 POSIX 平台上。基于文件描述符的 I/O 操作是 Linux 中最常用的操作之一。
三、linux下文件操作之系统调用函数
1.open() ,create()函数
在linux下,open()函数用于打开一个已经存在的文件或者创建一个新文件,而create()函数用于创建一个新的文件:
int open(constchar * pathname,int flags);
int open(constchar *pathname,int flags,mode_t mode);
int create(constchar *parhname,mode_t mode);
其中,flags为用户设置的标准,其可能值为:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(读写),O_APPEND(追加)
O_CREAT(不存在则创建)等等,这里特别注意的是,当我们选择了O_CREAT时,则要在mode参数中设定新文件的权限。
pathname为文件路径
mode参数用于表示打开文件的权限,它必须与flags的O_CREAT结合使用。
2.读取文件read函数
ssize_t read(int fd,void *buf,size_t count)
read函数是负责从fd中读取count字节内容并且放在buf开始的缓冲区。当读成功时,文件对应的读取位置指针向后移动位置、移动的大小为成功读取的字节数,read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误。
3.写文件write()函数
ssize_t write(int fd, const void*buf,size_t count);
write函数将buf中的count字节内容写入文件描述符fd。成功时返回写的字节数,失败时返回-1,并设置errno变量。
4.文件偏移函数lseek()
每个打开的文件都有一个与其相关联的“当前文件偏移量”。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。
按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。可以调用lseek显式地为一个打开的文件设置起偏移量。
#include <unistd.h>
off_t lseek(int filedes, off_t offset,int whence);
返回值:成功返回新的文件偏移量,出错返回-1。对参数offset的解释与参数whence的值有关。
若是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
若是SEEK_CUR,则将该文件的偏移量设置为当前值加offset,offset可为正或负。
若是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可为正或负。
可以用下列方式确定打开文件的当前偏移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
这种方法也可用来确定所涉及的文件是否可以设置偏移量。如果文件描述符引用的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,则其偏移量必须是非负值。因为偏移量可能是负值,所有在比较lseek的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于-1。
lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成了一个空洞。位于文件中但没有写过的字节都被读为0。文件中的空洞并不要求在磁盘上占用存储区。
5.close
函数关闭一个已打开的文件
#include <unistd.h> int close(int fd);
返回值:成功返回0,出错返回-1并设置errno
参数fd
是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close
关闭,所以即使用户程序不调用close
,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
四、linux下文件操作之C库函数
1、fopen
函数原型 FILE *fopen(const char *path,cost char *mode)
作用:打开一个文件,返回指向该文件的指针
参数说明:第一个参数为欲打开文件的文件路径及文件名,第二个参数表示对文件的打开方式
注:mode有以下值:
r:只读方式打开,文件必须存在
r+:可读写,必须存在
rb+:打开二进制文件,可以读写
rt+:打开文本文件,可读写
w:只写,文件存在则文件长度清0,文件不存在则建立该文件
w+:可读写,文件存在则文件长度清0,文件不存在则建立该文件
a:附加方式打开只写,不存在建立该文件,存在写入的数据加到文件尾,EOF符保留
a+:附加方式打开可读写,不存在建立该文件,存在写入的数据加到文件尾,EOF符不保留
wb:打开二进制文件,只写
wb+:打开或建立二进制文件,可读写
wt+:打开或建立文本文件,可读写
at+:打开文本文件,可读写,写的数据加在文本末尾
ab+:打开二进制文件,可读写,写的数据加在文件末尾
由mode字符可知,上述如r、w、a在其后都可以加一个b,表示以二进制形式打开文件
返回值:文件打开了,返回一个指向该打开文件的指针(FILE结构);文件打开失败,错误上存error code(错误代码)
注意:在fopen操作后要进行判断,是否文件打开,文件真正打开了才能进行后面的读或写操作,如有错误要进行错误处理
例:FILE *pfile=fopen(const char*filename,"rb");
打开文件流还有一个支持宽字符的函数,如下
FILE *_wfopen(constwchar_t *filename,const wchar_t *mode)
2、fread
函数原型:size_t fread(void*buff,size_t size,size_t count,FILE* stream)
作用:从文件中读入数据到指定的地址中
参数:第一个参数为接收数据的指针(buff),也即数据存储的地址
第二个参数为单个元素的大小,即由指针写入地址的数据大小,注意单位是字节
第三个参数为元素个数,即要读取的数据大小为size的元素个素
第四个参数为提供数据的文件指针,该指针指向文件内部数据
返回值:读取的总数据元素个数
例:
int num,count;
int* pr=newint[num*count];
fread(pr, num*4, count,stream); // stream为fopen中返回的FILE指针
要将数据写入pr中,必须为pr分配内存,一个int为4个字节,所以要x4
3、fseek
函数原型:int fseek(FILE *stream,longoffset,int framewhere)
作用:重定位文件内部的指针
参数:第一个为文件指针,第二个是指针的偏移量,第三个是指针偏移起始位置
返回值:重定位成功返回0,否则返回非零值
需要注意的是该函数不是重定位文件指针,而是重定位文件内部的指针,让指向文件内部数据的指针移到文件中我们感兴趣的数据上,重定位主要是这个目的。
说明:执行成功,则stream指向以fromwhere为基准,偏移offset个字节的位置。执行失败(比方说offset偏移的位置超出了文件大小),则保留原来stream的位置不变
4、 fwrite
fwrite(constvoid*buffer,size_t size,size_t count,FILE*stream);
(1)buffer:是一个指针,对fwrite来说,是要输出数据的地址。
(2)size:要写入的字节数;
(3)count:要进行写入size字节的数据项的个数;
(4)stream:目标文件指针。
说明:写入到文件的哪里?这个与文件的打开模式有关,如果是r+,则是从filepointer指向的地址开始写,替换掉之后的内容,文件的长度可以不变;如果是a+,则从文件的末尾开始添加,文件长度加大,而且是fseek函数对此函数没有作用。
5、fputc函数
fputc函数的作用就是将一个字符写入到文件中,其调用形式为:
fputc(ch,pFile);
其中ch就是要写入的字符,pFile是指向FILE结构的指针,通过fopen函数打开文件即可获取pFile。
写入文件有可能会失败,但怎么才能知道是否正确写入到文件了呢?这时候就需要看fputc函数的返回值了,fputc函数如果成功的将字符写入到文件了,则其返回值就是写入的那个字符,如果失败,则返回EOF(End Of File的意思)。EOF是一个符号常量,在stdio.h中EOF被定义为-1,因此见到EOF把他当做-1就是了。
6、fgetc函数
知道fputc是做什么的了,fgetc基本也就知道了,这个就是从文件中读入一个字符的函数,其调用形式为:
ch=fgetc(pFile);
参数pFile和fputc函数的参数一样,只不过少了一个参数ch,跑到返回值这里了。当fgetc成功从文件中读入字符后,ch就是读取到的字符,如果读取失败,则ch=EOF。
7、fscanf()
从文件指针fp指向的文件中,按format中对应的控制格式读取数据,并存储在agars对应的变量中;
原型:fscanf(FILE *fp, const char *format, agars)
8、fprintf()
将agars(参数表)内各项的值,按format(格式控制字符串)所表示的格式,将数据格式为字符串的形式写入到文件指针fp指向的文件中。
原型:fprintf(FILE*fp, const char *format, agars)
例
fprintf
(stderr,
"Can not open inputfile.\n"
);
fprintf
(stream,
"%s%c"
, s, c);
char
name[20] = "Mary";
对于其输出格式参数,和printf()一样。
代码来自http://blog.chinaunix.net/uid-26663150-id-3171467.html
1.#include <sys/types.h>
2.#include <sys/stat.h>//该头文件包含文件权限“读写执行”设置
3.#include <fcntl.h>//该头文件包含open,read,write,close等系统调用
4.#include <stdio.h>
5.#include <errno.h>
6.
7.#define BUFFER_SIZE 1024
//argc: 整数,用来统计你运行程序时送给main函数的命令行参数的个数
//* argv[ ]: 字符串数组,用来存放指向你的字符串参数的指针数组,每一个元素指向一个参数
8.int main(int argc,char **argv)
9.{
10.int from_fd,to_fd;
11.int bytes_read,bytes_write;
12.char buffer[BUFFER_SIZE];
13.char *ptr;
14.
15. if(argc!=3) //传入的参数的个数是否符合要求,如下图的错误信息
16. {
17. fprintf(stderr,"Usage:%s fromfile tofile/n/a",argv[0]);
18. exit(1);
19. }
20.
21. /* 打开源文件,此处即是打开argv[1],也即是传入的第一个参数cp1文件 */
22. if((from_fd=open(argv[1],O_RDONLY))==-1)
23. {
24. fprintf(stderr,"Open %s Error:%s/n",argv[1],strerror(errno));
25. exit(1);
26. }
27.
28. /* 创建目的文件,也即是传入的第二个参数cp2,因为没有,所以即是创建cp2文件 */
29. if((to_fd=open(argv[2],O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR))==-1)
30. {
31. fprintf(stderr,"Open %s Error:%s/n",argv[2],strerror(errno));
32. exit(1);
33. }
34.
35. /* 以下代码是一个经典的拷贝文件的代码,把cp1文件的内容传到文件cp2中 */
36.
37. while(bytes_read=read(from_fd,buffer,BUFFER_SIZE)) //首先打开cp1文件,第二次读的时候若第一次读完了就读出的字节数为0,就退出循环了。
38. {
39. /* 一个致命的错误发生了 */
40. if((bytes_read==-1)&&(errno!=EINTR)) break;
41. else if(bytes_read>0) //打开成功
42. {
43. ptr=buffer;
while(bytes_write=write(to_fd,ptr,bytes_read)) //向另一文件cp2写入cp1的内容
{
44. /* 一个致命错误发生了 */
if((bytes_write==-1)&&(errno!=EINTR))break;
45. /* 写完了所有读的字节 */
else if(bytes_write==bytes_read) break;
46. /* 只写了一部分,继续写 */
else if(bytes_write>0)
{
ptr+=bytes_write; //ptr= ptr+ bytes_write(写过的末尾处)
bytes_read-=bytes_write; //将要写的长度=刚才长度-已写长度
}
}
47. /* 写的时候发生的致命错误 */
if(bytes_write==-1)break;
}
48. }
49.close(from_fd);
50.close(to_fd);
51.exit(0);
52.}