0 系统调用
0.1 什么是系统调用
系统调用函数属于操作系统的一部分,是为了提供给用户进行操作的接口(API函数);
使得用户态运行的进程与硬件设备(如CPU、磁盘、打印机、显示器)等进行交互。
常见的系统调用有:
- write
- read
- open
- …
0.2 什么是库函数
库函数可以理解为是对系统调用函数的一层封装。库函数可分为两类:
- 一类是C语言标准库函数;
- 一类是编译器特定的库函数。
尽管系统函数执行效率是比较高效而精简的,但有时我们需要对获取的信息进行更复杂的处理,或更人性化的需要,我们把这些处理过程封装成一个函数,再将许多这类的函数放在一个文件(库)一般放在 .lib文件。最后再供程序员使用。
使用的时候包含头文件就可以使用其中的库函数了
#include<stdio.h>
就可以使用常见的库函数:
- printf
- fwrite
- fread
- fopen
- …
0.3 将hello写入到文件1.txt流程
- 首先fopen打开文件;
- fwrite参数附上要写入的内容;
- 文本内容来到C标准缓冲区;
- 如果满足条件就刷新C标准缓冲区,调用系统函数write进行写(注:满了就会自动刷新);
- write却只是把要写入的内容写到内核缓冲区;
- 如果内核缓冲区满足条件就刷新内核缓冲区,系统调用sys_write将缓冲区内容写入到磁盘(注:有进程会定时刷新内核缓冲区);
- 此时如果有进程要读取1.txt文件内容,发现内核缓冲区就有这个文件内容,就直接从内核缓冲区读取。
0.4 为什么要有缓冲区(补充)
定义:缓冲区就是内存里的一块区域,把数据先存内存里,然后一次性写入硬盘中的文件,类似于数据库的批量操作。
好处:减少对硬盘的直接操作,硬盘的执行速度为毫秒级别,内存为纳秒级别。在硬盘直接操作读写效率太低。
0.5 C标准缓冲区和内核缓冲区的区别
- C语言标准库函数fopen()每打开一个文件时候,其都会对应一个单独一个缓冲区;
- 而内核缓冲区是公用的。
1 文件I/O
open()
read()
write()
lseek()
close()
1.1 open函数
#include <fcntl.h>
int open(const char *pathname, int flags[, mode_t mode);
open函数参数说明:
- pathname:待打开文件的文件路径名;
- flags:访问模式,常用的宏有:
– O_RDONLY:只读
– O_WRONLY: 只写
– O_RDWR: 读写
– O_CREAT: 创建一个文件并打开
– O_EXCL: 测试文件是否存在,不存在则创建
– O_TRUNC: 以只写或读写方式成功打开文件时,将文件长度截断为0
– O_APPEND: 以追加方式打开文件
只有第二个参数flags = O_CREAT,第三个参数才会被用于设置新文件的权限,取值如下:
- S_IRWXU: 文件所有者,读、写、执行
- S_IRUSR: 文件所有者,读
- S_IWUSR: 文件所有者,写
- S_IXUSR: 文件所有者,执行
- S_IRWXG: 文件所属组,读、写、执行
- S_IRGRP: 文件所属组,读
- S_IWGRP: 文件所属组,写
- S_IXGRP: 文件所属组,执行
- S_IRWXO: 其他人,读、写、执行
- S_IROTH: 其他人,读
- S_IWOTH: 其他人,写
- S_IXOTH: 其他人,执行
返回值说明:
- 调用成功,返回一个文件描述符
- 不成功,返回-1
例: 创建一个文件
open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode);
or
int create(const char *pathname, mode_t mode);
【案例 1】创建一个文件,设置权限为0777。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(){
int tempFd;
tempFd = open("abc", O_CREAT, 0777);
printf("fd = %d\n", tempFd);
return 0;
}//of main
问题:(1) 为什么fd = 3? (2) 为什么abc文件权限是775?
[答]: (1) 有stdin, stdout, stderr三个文件默认打开; (2) 文件权限是设置的权限和掩码进行与运算后的结果。
【案例 2】最多能打开多少个文件?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[]){
int tempFd;
char tempName[1024];
int i = 0;
while (1) {
sprintf(tempName, "file%d", ++i);
tempFd = open(tempName, O_CREAT, 0777);
if (tempFd == -1) {
exit(1);
}//of if
printf("%d\n", i);
}//of while
return 0;
}//of main
问题:为什么是1021?不是1024?
答:因为有stdin, stdout, stderr三个文件默认打开
几个有用的命令:
- cat /proc/sys/fs/file-max
查看当前系统允许打开最大文件个数 - ulimit –a
当前默认设置最大打开文件个数1024 - ulimit –n 4096
修改默认设置最大打开文件个数为4096
1.2 read函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
read函数参数说明:
- fd: 从open或create函数返回的文件描述符
- buf: 缓冲区
- count: 读取数据的字节数
返回值说明:
- ssize_t: 有符号的size_t,有三种返回值
– 正数:请求读取的字节数
– 0: 文件长度有限,若读写位置距文件末尾只有20字节,该函数请求读取30字节,则第一次读取时返回值为20,第二次读取时,返回0
– -1: 读取文件出错
特殊说明: read函数从设备或网络中读数据,如从终端读取数据,终端写入数据没回车,这些数据不会传给read函数,read函数就会一直阻塞;如从网络端读取数据,网络通信的socket文件没有数据,read函数同样会阻塞。
【案例 1】阻塞读,超过字符数会出现什么问题。
#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);
}//of if
write(STDOUT_FILENO, buf, n);
return 0;
}//of main
问题:为什么会出现命令找不到?
答:超过10个字符,read只读取了10个字符,剩下的字符保存在内核的终端设备输入缓冲区
a.out退出,Shell进程继续从终端读走剩下的字符d和换行符,把它当成一条命令,结果发现执行不了。
1.3 write函数
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t count);
write函数参数说明: 同read函数
返回值说明: 返回写入的字节数或者-1并设置errno
特殊说明: 向终端或网络端写数据时,可能会进入阻塞状态
【案例 1】
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int fd;
char text[]="Welcome come to SWPU";
fd=open("tmp.txt",O_WRONLY|O_CREAT,0777);
write(fd,text,strlen(text));
1.4 lseek函数
#include <unistd.h>
ssize_t lseek(int fd, off_t offset, int whence);
lseek函数参数说明:
- fd: 从open或create函数返回的文件描述符
- offset: 对文件偏移量的设置,参数可正可负
- whence: 控制设置当前文件偏移量的方法
– whence = SEEK_SET: 偏移到文件头+ 设置的偏移量
– whence = SEEK_CUR: 偏移到当前位置+设置的偏移量
– whence = SEEK_END: 偏移到文件尾+设置的偏移量
返回值说明:
- 设置成功:返回新的偏移量
- 不成功:-1
【案例1】返回当前的偏移量
int fd,ret;
fd=open("hello1.txt",O_RDWR);
ret=lseek(fd,0,SEEK_CUR);
printf("%d\n",ret);
【案例2】返回文件大小
int fd,ret;
fd=open("hello1.txt",O_RDWR);
ret=lseek(fd,0,SEEK_END);
printf("%d\n",ret);
【案例3】扩充文件大小:特别注意扩充文件大小后 需要写入内容 否则扩充不生效
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd,ret;
char a[]="Welcome to SWPU";
fd=open("hello1.txt",O_RDWR|O_CREAT,0777);
ret=lseek(fd,1000,SEEK_END);
write(fd,a,strlen(a));
printf("%d\n",ret);
return 0;
}
使用ls -l 命令可以查看扩充后的字节大小
1.5 close函数
#include <unistd.h>
int close(int fd);
返回值说明:
- 成功:返回0
- 不成功:-1
2 案例
【案例 4】 使用open函数打开或创建一个文件,将文件清空,使用write函数在文件中写入数据,并使用read函数将数据读取并打印。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(){
int tempFd = 0;
char tempFileName[20] = "test.txt";
//Step 1. open the file.
tempFd = open(tempFileName, O_RDWR|O_EXCL|O_TRUNC, S_IRWXG);
if(tempFd == -1){
perror("file open error.\n");
exit(-1);
}//of if
//Step 2. write the data.
int tempLen = 0;
char tempBuf[100] = {0};
scanf("%s", tempBuf);
tempLen = strlen(tempBuf);
write(tempFd, tempBuf, tempLen);
close(tempFd);
//Step 3. read the file
tempFd = open(tempFileName, O_RDONLY);
if(tempFd == -1){
perror("file open error.\n");
exit(-1);
}//of if
off_t tempFileSize = 0;
tempFileSize = lseek(tempFd, 0, SEEK_END);
lseek(tempFd, 0, SEEK_SET);
while(lseek(tempFd, 0, SEEK_CUR)!= tempFileSize){
read(tempFd, tempBuf, 1024);
printf("%s\n", tempBuf);
}//of while
close(tempFd);
return 0;
}//of main
3 虚拟空间地址/PCB进程控制块/文件描述符表
对于每一个进程,系统都会为其分配一个0-4G的虚拟空间地址:
- 0-3G为用户空间,用户可操作部分;
- 3~4G为内核,其中PCB控制块也存在内核中,PCB结构体包括文件标识符表以及其他很多信息(补充:每一个进程都有一个PCB)。
文件描述符表:结构体 PCB 的成员变量 file_struct *file 指向文件描述符表。
从应用程序使用角度,该指针可理解记忆成一个字符指针数组,下标 0、1、2、…,通过该指针找到文件结构体。本质是一个键值对 0、1、2、…都分别对应具体地址。但键值对使用的特性是自动映射,我们只操作键不直接使用值。
新打开文件返回文件描述符表中未使用的最小文件描述符。