文件I/O
1.C标准函数与系统函数的区别1.c标准函数和系统函数的区别
接下来用应用层API的知识。每当打开一个文件,默认打开标准输入,标准输出,标准出错流三个流,每个FILE都对应一个缓冲区,默认大小为8192Byte。
2.PCB概念(进程控制块)
(1)task_struck结构体
可以自己动手查看 vi /usr/src/linux-headers/includelinuxsched.h
在linux中,把每一个进程的基本信息抽象成一个结构体,这就是task_struct结构体,在includelinuxsched.h文件中定义。每个进程都会被分配一个task_struct结构,它包含了这个进程的所有信息。在任何时候,操作系统都能跟踪这个结构的信息。
struct task_struct { volatile long state; / /这个是进程的运行时状态,-1代表不可运行,0代表可运行,>0代表已停止。
处于这种状态的进程,要么正在运行、要么正准备运行。正在运行的进程就是当前进程(由current所指向的进程),而准备运行的进程只要得到CPU就可以立即投入运行,CPU是这些进程唯一等待的系统资源。系统中有一个运行队列(run_queue),用来容纳所有处于可运行状态的进程,调度程序执行时,从中选择一个进程投入运行。在后面我们讨论进程调度的时候,可以看到运行队列的作用。当前运行进程一直处于该队列中,也就是说,current总是指向运行队列中的某个元素,只是具体指向谁由调度程序决定。
等待状态 TASK_INTERRUPTIBLE可中断 TASK_UNINTERRUPTIBLE不可中断
处于该状态的进程正在等待某个事件(event)或某个资源,它肯定位于系统中的某个等待队列(wait_queue)中。Linux中处于等待状态的进程分为两种:可中断的等待状态和不可中断的等待状态。处于可中断等待态的进程可以被信号唤醒,如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度;而处于不可中断等待态的进程是因为硬件环境不能满足而等待,例如等待特定的系统资源,它任何情况下都不能被打断,只能用特定的方式来唤醒它,例如唤醒函数wake_up()等。
暂停状态TASK_STOPPED
此时的进程暂时停止运行来接受某种特殊处理。通常当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。
僵死状态TASK_ZOMBIE
进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。
(2)files struck 每个PCB里边有一个,本质是一个数组。
(3)open/close
转自:http://joe.is-programmer.com/posts/17463.html
open函数可以打开或创建一个文件。
#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);
返回值:成功返回新分配的文件描述符,出错返回-1并设置errno
pathname参数是要打开或创建的文件名,和fopen一样,pathname既可以是相对路径也可以是绝对路径。
flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以O_开头,表示or。
必选项:以下三个常数中必须指定一个,且仅允许指定一个。
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 可读可写打开
以下可选项可以同时指定0个或多个,和必选项按位或起来作为flags参数。
O_APPEND 表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容。
O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示该文件的访问权限。
O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。
O_TRUNC 如果文件已存在,并且以只写或可读可写方式打开,则将其长度截断(Truncate)为0字节。
O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O(Nonblock I/O)。
第三个参数mode指定文件权限,可以用八进制数表示,比如0644表示-rw-r--r--,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示,详见open(2)的Man Page。要注意的是,文件权限由open的mode参数和当前进程的umask掩码共同决定。注意:创建文件权限不能大于执行程序用户的自有权限,如O_CREAT选项时,忘加mode参数,则新创建的文件从栈帧中取出垃圾值为mode。
close函数关闭一个已打开的文件:
#include <unistd.h> int close(int fd);
返回值:成功返回0,出错返回-1并设置errno
参数fd是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
给个例子:
//filename:code // #include <error.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>
#include<stdio.h>
#include<unistd.h>
int main(void) { int fd;
char buf[1024]="helloworld";
fd=open("abc",O_CREAT|O_RDWR,0777);
write(fd,buf,strlen(buf));
printf("fd=%d ",fd);
close(fd);
return 0; }
执行:gcc open.c -o app 查看结果:$umask 0002 $ls -l abc -rwxrwxr–x 1 joseph joseph 0 11月7日 10:24 abc 以上例子以mode 0777创建一个文件mytest,而umask是0002, 所以创建的文件权限是0777-0002=0775(-rwxrwxr–x)。
最大打开文件个数
一般默认为1024,查看方式
ulimit -a
其中的open files 就是,改变方式 ulimit -n 文件数,最多能打开个个数,查看方式 cat /proc/sys/fs/file-max,数目与内存的大小有关
(4)read/write
read函数从打开的设备或文件中读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0
参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:
1、读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。
2、从终端设备读,通常以行为单位,读到换行符就返回了。
3、从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数。
write函数向打开的设备或文件中写数据。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:成功返回写入的字节数,出错返回-1并设置errno
写常规文件时,write的返回值通常等于请求写的字节数count,而向终端设备或网络写则不一定。
读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
实例:
1 #include<unistd.h> 2 #include<sys/stat.h> 3 #include<sys/types.h> 4 #include<fcntl.h> 5 #include<stdlib.h> 6 #include<strio.h> 7 8 #define SIZE 8192 9 10 int main(int argc,char *argv[]) 11 { 12 char buf[SIZE]; 13 int fd_src,fd_dest,len; 14 if(argc<3) 15 { 16 printf("./mycp src dest "); 17 exit(1); 18 } 19 fd_src=open(argv[1],O_RDONLY); 20 fd_dest=open(argv[2],O_CREAT|O_WRONLY|O_TRUNC,00644); 21 22 while((len=read(fd_src,buf,sizeof(buf))>0) 23 { 24 write(fd_dest,buf,len); 25 } 26 close(fd_src); 27 close(fd_dest); 28 return 0; 29 }
(5)阻塞和非阻塞(阻塞相当于c++中的cin,一直等待键盘输入)
转自http://blog.csdn.net/u012317833/article/details/39343915
阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep
指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:
-
正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(
eip
)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。 -
就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。
下面这个小程序从终端读数据再写回终端。
阻塞读终端
-
#include <unistd.h> #include <stdlib.h> int main(void) { char buf[10]; int n; n = read(STDIN_FILENO, buf, 10); if (n < 0) { perror("read STDIN_FILENO"); exit(1); } write(STDOUT_FILENO, buf, n); return 0; }
执行结果如下:
-
[root@localhost apue2]# ./a.out hello hello [root@localhost apue2]# ./a.out hello world hello worl[root@localhost apue2]# d bash: d: command not found [root@localhost apue2]#
第一次执行a.out
的结果很正常,而第二次执行的过程有点特殊,现在分析一下:
-
Shell进程创建
a.out
进程,a.out
进程开始执行,而Shell进程睡眠等待a.out
进程退出。 -
a.out
调用read
时睡眠等待,直到终端设备输入了换行符才从read
返回,read
只读走10个字符,剩下的字符仍然保存在内核的终端设备输入缓冲区中。 -
a.out
进程打印并退出,这时Shell进程恢复运行,Shell继续从终端读取用户输入的命令,于是读走了终端设备输入缓冲区中剩下的字符d和换行符,把它当成一条命令解释执行,结果发现执行不了,没有d这个命令。
如果在open
一个设备时指定了O_NONBLOCK
标志,read
/write
就不会阻塞。以read
为例,如果设备暂时没有数据可读就返回-1,同时置errno
为EWOULDBLOCK
(或者EAGAIN
,这两个宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询(Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:
-
while(1) { 非阻塞read(设备1); if(设备1有数据到达) 处理数据; 非阻塞read(设备2); if(设备2有数据到达) 处理数据; ... }
如果read(设备1)
是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read
调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处理。
非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻塞I/O时,通常不会在一个while
循环中一直不停地查询(这称为Tight Loop),而是每延迟等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行
-
while(1) { 非阻塞read(设备1); if(设备1有数据到达) 处理数据; 非阻塞read(设备2); if(设备2有数据到达) 处理数据; ... sleep(n); //多出来的部分 }
这样做的问题是,设备1有数据到达时可能不能及时处理,最长需延迟n秒才能处理,而且反复查询还是做了很多无用功。以后要学习的select(2)
函数可以阻塞地同时监视多个设备,还可以设定阻塞等待的超时时间,从而圆满地解决了这个问题。
以下是一个非阻塞I/O的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我们用终端来做这个实验。程序开始执行时在0、1、2文件描述符上自动打开的文件就是终端,但是没有O_NONBLOCK
标志。读标准输入是阻塞的。我们可以重新打开一遍设备文件/dev/tty
(表示当前终端),在打开时指定O_NONBLOCK
标志。
非阻塞读终端
-
#include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <stdlib.h> #define MSG_TRY "try again " int main(void) { char buf[10]; int fd, n; fd = open("/dev/tty", O_RDONLY|O_NONBLOCK); if(fd<0) { perror("open /dev/tty"); exit(1); } tryagain: //轮询模型
n = read(fd, buf, 10); if (n < 0) { if (errno == EAGAIN) { //EAGAIN 一种出错,再试一次) sleep(1); write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY)); goto tryagain; } perror("read /dev/tty"); exit(1); } write(STDOUT_FILENO, buf, n); close(fd); return 0; }
以下是用非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到达时处理延迟较小。
非阻塞读终端和等待超时
-
#include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <stdlib.h> #define MSG_TRY "try again " #define MSG_TIMEOUT "timeout " int main(void) { char buf[10]; int fd, n, i; fd = open("/dev/tty", O_RDONLY|O_NONBLOCK); if(fd<0) { perror("open /dev/tty"); exit(1); } for(i=0; i<5; i++) { n = read(fd, buf, 10); if(n>=0) break; if(errno!=EAGAIN) { perror("read /dev/tty"); exit(1); } sleep(1); write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY)); } if(i==5) write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT)); else write(STDOUT_FILENO, buf, n); close(fd); return 0; }
注:error和perror - perror会根据errno返回的序号,查找对应的出差信息并打印