相关知识
Linux中是号称一切皆文件的,我们对文件的操作主要有打开,关闭,读,写操作。
-
打开 int open(char *filename , int flags ,mode_t mode)
open函数将filename转化为一个文件描述符,并且返回文件描述符数字,返回的文件描述符数字是进程中没有打开的最小的描述符。
flags指定以什么方式打开- O_RDONLY: 只读
- O_WRONLY: 只写
- O_RDWR: 可读可写
还有其它一些方式: - O_CREAT:如果文件不存在,就创建一个截断(空)的文件
- O_TRUNC:如果文件已经存在,就截断它
- O_APPEND:在文件末尾添加
mode参数指定了文件的访问权限(该参数只有在创建文件才有作用),如:S_IRUSR 使用者能够读这个文件。
-
关闭 int close(int fd) 成功返回0,出错返回-1
注意:关闭一个已经关闭的描述符会出错 -
读 ssize_t read(int fd,void *buf,size_t n)
成功则返回读的字节数,若EOF则为0,出错则为-1,从文件描述符为fd的文件读取n个字节到内存buf处。 -
写 ssize_t write(int fd,const void *buf,size_t n)
成功则为写的字节数,出错则为-1。从内存位置buf复制至多n个字节到文件描述符fd的当前文件位置。
再看看文件描述符表,文件表,v-node表是什么。
- 文件描述符表 每个进程有自己的描述符表,每个表项是由进程中的打开的文件描述符来索引,意思就是,每个进程打开的文件都可以在这边找到一个对应的文件描述符,每个打开的文件描述符指向文件表中的一个表项。
- 文件表 文件表是由打开的文件集合组成的,所有进程共享这张表,每个表项组成包括当前的文件位置,引用计数(当前指向该表项的描述符表项数),关闭一个描述符会减少相应的文件表表项中的引用计数,内核直到引用计数为0,才会删除这个文件表表项。还有一个指向v-node表中对应文件的指针。
- v-node 所以进程共享,包含stat结构中的打大多数信息,stat结构包括文件的描述信息,比如文件大小,文件类型等。
第一个程序
#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,文件中为abcde。*/
首先,看这个程序如何运行,因为这个程序自己写了头文件,运用gcc file1.c csapp.h csapp.c -lpthread -o file1 就可以正常编译运行了。
这段程序为在文件中读3个字符,最关键的就是dup2函数怎么理解。
int dup2(int oldfd,int newfd)成功则为非负的描述符,出错为-1。
复制文件描述符表表项oldfd到描述符表项newfd。
程序分析
- 首先字符指针为第二个参数,即文件名,三个open函数以只读方式打开同一个文件,每个文件表表项是独立的。
- dup2函数将fd2复制到fd3,即在fd3中读就是在fd2中读,c1读了第一个字符a,c2读了第一个字符a,第三个read函数本来是在读fd3指的文件表项(即本来也应该读a),但是前面用dup2函数,就改为读fd2了,因为fd2已经读到了第二个字符,所以c3读的是b。
第二个程序
#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;
}
问在文件中写的内容是什么。
程序分析
- 打开第二个参数指定的文件,返回的描述符给fd1,方式为如果文件不存在就创建一个空文件,存在就清空,以可读可写的方式打开,模式为所有者可读可写。
- 向fd1所指的文件中写了“pqrs”,此时文件里的内容是pqrs,又以追加方式打开了同一个文件,返回的描述符给fd3,在fd3所指的文件后面写了“jklmn”,此时文件里的内容是pqrsjklmn,然后用了dup函数,这个函数复制文件描述符fd1给返回的文件描述符fd2,此时fd2所指的文件的光标是在第五个字符,然后向fd2所指的文件中写了“wxyz”,这4个字符覆盖了前面所写的“jklm”,此时文件里的内容是pqrswxyzn,然后在fd3所指的文件后面追写了“ef”。所以答案就是pqrswxyznef。
第三个程序
#include "csapp.h"
int main(int argc, char *argv[])
{
int fd1;
int s = getpid() & 0x1;
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;
}/*读的是abcde.txt,其中内容为abcde
程序分析
-
getpid()函数是获得进程号,再与0x1作位与运算,注意&和&&是不一样的,其实getpid()&0x1的结果就取决于getpid()的二进制的最低位是什么,如果是0,则最终结果是0,是1,最终结果是1。
-
打开所给文件,从其中读一个字符放到c1中,即读了字符“a”,文件位置改为第二个字符处。然后fork()。这时产生一个子进程,子进程会有一个父进程描述符表的副本,但父子进程共享相同的打开文件集合,因此共享相同的文件位置。
-
父子进程中都执行了sleep函数,一个进程不休眠,则另一个进程休眠1秒。所以输出结果是先执行父进程还是执行子进程,是由父进程进程号的二进制最低位决定的。
-
现在假设先执行父进程,向文件中读一个字符,这时的文件位置已经到了第二个字符处了,所以父进程中的c2读了字符“b”,输出语句。
-
然后执行子进程,从文件中读取一个字符,因为父子进程共享相同的打开文件集合,故此时的文件位置为第三个位置处,即子进程中的c2读了字符“c”,输出结果。
运行截图
第四个程序
/* $begin cpstdin */
#include "csapp.h"
int main(void)
{
char c;
while(Read(STDIN_FILENO, &c, 1) != 0)
Write(STDOUT_FILENO, &c, 1);
exit(0);
}
- 这个程序的循环终止条件时read函数返回值=0,即碰到EOF时,所以这个程序要想终止,就得碰到EOF,可知在键盘上按Ctrl+D就是EOF,则程序就终止了。
- 程序里面有两个宏定义STDIN_FILENO和STDOUT_FILENO,一个是标准输入,一个是标准输出。文件描述符就是0和1,另外2表示的是文件错误。
第五个程序
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("h");
printf("e");
printf("l");
printf("l");
printf("o");
printf("\n");
fflush(stdout);
exit(0);
}
这个程序是用来了解缓冲区的知识的。 以上输出为hello,并且换行。
现在考虑以下措施
- 删掉 fflush(stdout),这时程序输出hello并且换行
- 删掉printf("\n"),发现输出hello,不换行,fflush(stdout)是手动刷新缓冲区,将缓冲区里的内容输出。
- 删掉 fflush(stdout)和printf("\n"),发现程序也是输出hello,不换行,这是因为程序终止,将缓冲区里的内容输出。
- 现在删掉 fflush(stdout)和printf("\n"),将exit(0)改为_exit(0),发现不输出hello了,因为_exit(0)是退出时不会清除缓冲区,也就不会将hello输出了。