系统级IO
csapp.c文件中包含大量封装函数,已对-1的返回情况做了处理,以下调用的函数有的用大写表示(已处理)。见第八章。
程序运行加-lpthread(多线程),这里考虑到csapp.h
Unix I/O
所有的I/O设备(如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写执行)
open,close,read,write,lseek。
- 改变当前文件的位置。每个打开的文件内核保持一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。执行seek操作能将文件的当前位置设置为k。
共享文件
- 描述符表。一个进程一个描述符表。描述表的表项是由进程打开的文件描述符索引的。每个打开的文件描述符表项指向文件表中的一个表项。
- 文件表。进程共享。打开文件的集合是由文件表来表示。文件表的表项组成包括当前的文件位置、引用计数(refcnt)即指向该表项的描述符表项数、一个指向v-node表中对应表象的指针。
- v-node表。进程共享。包含stat结构的大多数信息包括st_mode和st_size成员。
I/O重定向
原本Linux认为标准输出是显示屏,现在输出到文件。
dup2函数复制描述符表项oldfd到newfd,覆盖newfd以前的内容。若newfd已打开,会在复制oldfd前关闭newfd。
int dup2(int oldfs, int newfd);
返回:成功则为fd,出错为-1
打开和关闭文件:
- 打开文件。一个应用程序要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数叫描述符。内核记录有关这个打开文件的所有信息。
Linux shell创建的每个进程开始时都有3个打开的文件:标准输入(fd=0)、标准输出(fd=1)和标准错误(fd=2)。
int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并返回描述符数字。flags参数指明了子进程的访问方式,也可以是一个或更多位掩码的或;mode参数指定了新文件的访问权限位。
返回:成功为新文件描述符(最小的,但>=3),出错为-1。
- 关闭文件。内核释放文件打开是创建的数据结构,并将这个描述符恢复到可用的描述符池。
int close(int fd);
读和写文件
size_t被定义为unsigned long。size_t被定义为long。
- 读文件。从文件复制n>0个字节到内存,从k开始,然后k变为k+n。若文件字节大小为m,k>=m时执行读操作会触发EOF的条件。
read函数从描述符为fd的当前文件位置复制最多n个字节最多n个字节到内存位置buf。
ssize_t read(int fd, void *buf, size_t n);
返回:成功则为读的字节数,读到文件末尾(EOF)返回0,出错返回-1。
- 写文件。从内存复制n>0个字节到一个文件,从当前位置k开始,然后更新k。
从内存buf复制至多n个字节到描述符fd的当前文件位置。
ssize_t write(int fd, const void *buf, size_t n);
返回:成功则为写的字节数,出错则为-1。
/*abcde.txt*/
csapp
ffiles1:
#include "csapp.h"
int main(int argc, char *argv[])
{
int fd1, fd2, fd3;
char c1, c2, c3;
char *fname = argv[1];
fd1 = Open(fname, O_RDONLY, 0);
fd2 = Open(fname, O_RDONLY, 0);
fd3 = Open(fname, O_RDONLY, 0);
dup2(fd2, fd3);
Read(fd1, &c1, 1);
Read(fd2, &c2, 1);
Read(fd3, &c3, 1);
printf("c1 = %c, c2 = %c, c3 = %c\n", c1, c2, c3);
Close(fd1);
Close(fd2);
Close(fd3);
return 0;
}
以只读的方式将abcde.txt文件打开了3次,然后重定向时dup2函数将描述符表项fd2复制到fd3,覆盖fd3的内容。
每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据(联系读写文件时的k值变化)。在fd1,fd2指向的文件中分别读入一个字节后,k值为1,因此这里c1与c2值都为第一个字节c。
dup2执行之后fd3原来指向的文件已关闭,现在指向的是fd2所指向的文件,fd3文件表和v-node表表项被删除。从此以后任何标准输出的数据都被重定向到fd2指向的文件。因此fd3在fd2指向的文件中继续读数据,此时再读一个字节,k值为2,c3值为第二个字节s。
ffiles2:
#include "csapp.h"
int main(int argc, char *argv[])
{
int fd1;
int s = getpid() & 0x1;
printf("pid = %d\n",getpid());
printf("s = %d\n",s);
char c1, c2;
char *fname = argv[1];
fd1 = Open(fname, O_RDONLY, 0);
Read(fd1, &c1, 1);
if (fork()) {
/* Parent */
sleep(s);
Read(fd1, &c2, 1);
printf("Parent: c1 = %c, c2 = %c\n", c1, c2);
} else {
/* Child */
sleep(1-s);
Read(fd1, &c2, 1);
printf("Child: c1 = %c, c2 = %c\n", c1, c2);
}
return 0;
}
int s = getpid() & 0x1;
由于getpid的返回值不确定,这里会将s值设置为1或0。
若设置为1,Child(sleep(0))将会快于Parent(sleep(1));
若设置为0,Parent(sleep(0))将会快于Child(sleep(1));
现在解释s设置为1时,Child和Parent的输出情况。
首先,在fork还未创建子进程时,以只读的方式将abcde.txt文件打开了1次,返回fd1,然后读入一个字节到c1,此时文件的当前位置k=1,子进程中sleep(0)相当于没有等待,而子进程相当于有一个父进程描述符表的副本,父子进程共享相同的打开文件表集合,因此共享相同的位置,所以子进程读文件是也是从k=1开始读的,然后读入一个字节到c2,此时k=2,输出Child:c1=c,c2=s。
之后在sleep(1)后父进程开始read,父进程依旧是从fd1中读入一个字节,而打开文件表是所有进程共享的,之前子进程读了一个字节,所以父进程是从k=2开始读,然后再读入一个字节到c2,此时k=3,输出Parent:c1=c,c2=a。
s设置为0的情况同理。
ffiles3:
#include "csapp.h"
int main(int argc, char *argv[])
{
int fd1, fd2, fd3;
char *fname = argv[1];
fd1 = Open(fname, O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR);
Write(fd1, "pqrs", 4);
fd3 = Open(fname, O_APPEND|O_WRONLY, 0);
Write(fd3, "jklmn", 5);
fd2 = dup(fd1); /* Allocates new descriptor */
Write(fd2, "wxyz", 4);
Write(fd3, "ef", 2);
Close(fd1);
Close(fd2);
Close(fd3);
return 0;
}
dup函数:int dup(int oldfd);
dup用来复制参数oldfd所指的文件描述符。当复制成功是,返回最小的尚未被使用过的文件描述符,若有错误则返回-1。返回的新文件描述符和参数oldfd指向同一个文件,这两个描述符共享同一个数据结构,共享所有的锁定,读写指针和各项全现或标志位。
以可读可写的方式打开(O_RDWR;若文件不存在就创建一个截断的文件(O_CREAT);若存在就截断它,即清空文件(O_TRUNC)。这里命令行的第二个参数为文件abcde.txt,因此该文件打开时会被清空,返回fd1,然后从pqrs字符串复制4个字节到abcde.txt。
以只写权限打开文件(O_WDONLY),在每次操作前设置文件的位置到文件的末尾(O_APPEND)。因此abcde.txt文件是以只写方式打开的,返回fd3,打开时已在文件的末尾,即 k = 4,(没有注明O_APPEND则这里还是 k = 0),再继续从jklmn字符串复制5个字节到abcde.txt,此时文件里的数据应是pqrsjklmn,k = 9。
由dup函数的功能知fd2会指向fd1所指向的文件abcde.txt,拥有同一张文件表,然后从wxyz字符串复制4个字节到abcde.txt,但是由于之前fd1执行写操作后 k = 4,这时候fd2继续写的时候是从当前位置 k = 4 开始的,wxyz会覆盖原来的jklm,此时文件的数据应是pqrswxyzn。
接着将ef复制到fd3的当前位置,从 k = 9开始写入,此时文件里的数据应是pqrsjklmnef,k = 11。
cpstdin:
/* $begin cpstdin */
#include "csapp.h"
int main(void)
{
char c;
while(Read(STDIN_FILENO, &c, 1) != 0)
Write(STDOUT_FILENO, &c, 1);
exit(0);
}
STDIN_FILENO表示从键盘接收,是(fd)0的宏定义;STDOUT_FILENO表示从显示屏输出,是(fd)1的宏定义;当一个字符一个字符的输入csapp后,输入回车符就会再从缓冲区输出一个csapp。但并不会跳出循环。
Read(STDIN_FILENO, &c, 1)相当于C语言中的getchar();
statcheck:
/* $begin statcheck */
#include "csapp.h"
int main (int argc, char **argv)
{
struct stat stat;
char *type, *readok, *writeok, *executeok;
/* $end statcheck */
if (argc != 2) {
fprintf(stderr, "usage: %s <filename>\n", argv[0]);
exit(0);
}
/* $begin statcheck */
Stat(argv[1], &stat);
if (S_ISREG(stat.st_mode)) /* Determine file type */
type = "regular";
else if (S_ISDIR(stat.st_mode))
type = "directory";
else
type = "other";
if ((stat.st_mode & S_IRUSR)) /* Check read access */
readok = "r_yes";
else
readok = "r_no";
if ((stat.st_mode & S_IWUSR)) /* Check write access */
writeok = "w_yes";
else
writeok = "w_no";
if ((stat.st_mode & S_IXUSR)) /* Check execute access */
executeok = "e_yes";
else
executeok = "e_no";
printf("type: %s, read: %s\t%s\t%s\n", type, readok, writeok, executeok);
exit(0);
}
在执行ffiles的操作后,检查abcde.txt的信息(state数据结构中st_mode)。
这里注意当打开一个文件时,对包含该文件的每一个目录,都应该具有执行权限。而ffiles3程序中我们能知道这个文件具有S_IRUSER和S_IWUSER,经statcheck程序检验知道,它的确具有user的读写执行权限。
下面再看一个例子:
createmode:
#include "csapp.h"
int main(int argc, char *argv[])
{
int fd1;
fd1 = Open("foo.txt", O_CREAT|O_TRUNC|O_WRONLY, S_IRUSR);
Close(fd1);
return 0;
}
创建foo.txt时并没有给它用户的写权限,所以这里显示w_no。