文章目录
一、系统调用
什么是系统调用:
由操作系统向应用程序提供的程序接口信息,也叫应用编程接口(Application Programming Interface,API),它是是应用程序与操作系统之间交互的接口。
操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境,使应用程序具有更好的兼容性,为了达到这个目的,内核提供了一系列具备了一定功能的内核函数,通过一组称为系统调用(system call)的接口呈现给用户。
系统调用负责把应用程序的请求传给内核,调用相应的内核函数完成所需的处理,然后将处理结果返回给应用程序。
UNIX/Linux大分部的系统功能是通过系统调用实现的,这些系统调用被封装成了C函数的形式,但它们并不是真正的函数。
标准库函数大部分工作在用户态,一部分函数会使用系统调用进入内核态(fopen/malloc),可以使用 time命令统计程序的在用户态和内核态的运行时间:
time ./<可执行文件>
real 0m0.002s # 总执行时间
user 0m0.000s # 用户态执行时间
sys 0m0.000s # 内核态执行时间
总执行时间 = 用户态执行时间 + 内核态执行时间 + 用户态和内核态切换消耗的时间
strace ./可执行文件名 # 追踪函数的调用过程
普通函数调用与系统调用的区别:
普通函数调用的步骤:
1、调用者会先把要传递的参数压入栈内存
2、根据函数名也就是函数地址跳转到函数所在的代码段位置
3、接下来从栈内存中弹出参数
4、定义局部变量扩展栈内存并执行函数中的相关代码
5、返回执行结果
6、销毁该函数的栈内存
7、回到调用处继续执行
系统调用的步骤:
1、程序中执行到系统调用位置,触发软件中断机制
1、通过软中断机制进入内核运行状态
2、然后内核负责把参数从用户空间拷贝到内核空间
3、然后根据中断编号再执行相关操作
4、等执行完毕后,再把执行结果内核空间拷贝到用户空间
5、返回到程序再继续执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4F8YVFzg-1662377012277)(C:\Users\DELL\Pictures\中断.jpg)]
二、一切皆文件
在UNIX和Linux系统下,把操作系统提供的服务和设备都抽象成了文件,因为这样可以给各种设备控制提供了一个简单而统一的接口,程序完全可以象访问普通磁盘文件一样,访问串行口、网络、打印机或其它设备。
在UNIX和Linux系统下中的文件具有特别重要的意义,因为它在Linux中,(几乎)一切皆文件,大多数情况下只需要使用五个基本系统调用 open/close/read/write/ioctl,即可实现对各种设备的输入和输出,Linux中的任何对象都可以被视为某种特定类型的文件,可以访问文件的方式访问之。
-
- 目录文件
-
- 设备文件
- A. 控制台:/dev/console
- B. 声卡:/dev/audio
- C. 标准输入输出:/dev/tty
- D. 空设备:/dev/null
-
- 普通文件
三、文件描述符
什么是文件描述符:
文件描述符是一种非负的整数,表示一个打开的文件,由系统调用(open/creat)返回,在后续操作文件时可以被内核空间引用,内核默认为每个进程打开三个文件描述符:
-
stdin 0 - 标准输入
-
stdout 1 - 标准输出
-
stderr 2 - 标准出错
在unistd.h中被定义为如下三个宏:
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
文件描述符与文件指针:
在Linux系统中打开文件后,内存中(内核区间)就会有一个内核对象,也就是记录该文件相关信息的结构体变量,但内核为了自己的安全不能把它的地址返回给用户,而且由于内核区间和用户区间的原因,返回也无法访问、使用。
而且一个进程可能会同时打开多份文件,所以操作系统就在内核区间创建了一张索引表,表的每一项都有一个指向已打开文件的内核对象,文件描述符就是索引表的主键,如果把索引表看作数组,那么文件描述符就是数组的下标,不同进程之间交换文件描述没有意义。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CBg5vHkU-1662377012278)(C:\Users\DELL\Pictures\文件描述符.png)]
C语言中使用文件指针代表打开的文件,文件指针指向进程的用户区间中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是文件描述符。
int fileno(FILE *stream);
功能:把文件指针转换成文件描述符
FILE *fdopen(int fd, const char *mode);
功能:把文件描述符转换成文件指针
四、文件的创建与打开
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
功能:打开文件
pathname:文件路径
flags:打开文件的方式
返回值:文件描述符,失败返回负值
int open(const char *pathname, int flags, mode_t mode);
功能:打开或创建文件
pathname:文件路径
flags:打开方式
mode:创建文件时的权限
返回值:文件描述符,失败返回负值
int creat(const char *pathname, mode_t mode);
功能:专门用来创建文件,但基本不使用,因为open函数完全具备它的功能。
注意:open/creat所返回的一定是当前未被使用的,最小文件描述符。
一个进程可以同时打开的文件描述符个数,受limits.h中定义的OPEN_MAX宏的限制,POSIX要求不低于16,传统UNIX是63,现代Linux是255。
flags:
O_APPEND 打开文件后位置指针指向末尾
O_CREAT 文件不存在时创建
O_RDONLY 只读权限
O_WRONLY 只写权限
O_RDWR 读写权限
O_TRUNC 清空文件内容
O_EXCL 如果文件存在则创建失败
O_NOCTTY 若pathname指向控制终端,则不将该终端作为控制终端。
O_NONBLOCK 若pathname指向FIFO/块/字符文件,则该文件的打开及后续操作均为非阻塞模式。
O_SYNC write等待数据和属性,被物理地写入底层硬件后再返回。
O_DSYNC write等待数据,被物理地写入底层硬件后再返回。
O_RSYNC read等待对所访问区域的所有写操作,全部完成后再读取并返回。
O_ASYNC 当文件描述符可读/写时,向调用进程发送SIGIO信号。
mode:
S_IRWXU 00700
S_IRUSR 00400
S_IWUSR 00200
S_IXUSR 00100
S_IRWXG 00070
S_IRGRP 00040
S_IWGRP 00020
S_IXGRP 00010
S_IRWXO 00007
S_IROTH 00004
S_IWOTH 00002
S_IXOTH 00001
int close (int fd);
功能:关闭文件,成功返回0,失败返回-1
问题1:C语言可以定义重名函数吗?
可以,但需要在不同的作用域下才可以重名。
情况1:在不同的源文件中,static函数可以与普通函数重名。
情况2:在函数内定义的函数可以与普通函数重名。
问题2:系统调用为什么可以重名?
因为系统调用不是真正的函数,而是借助软中断实现的,决定执行哪个系统调用的是中断编号,而不是名字。
问题3:r,w,a,r+,w+,a+分配对应哪些flags标志?
"r" O_RDONLY
"r+" O_RDWR
"w" O_WRONLY|O_CREAT|O_TRUNC, 0666
"w+" O_RDWR|O_CREAT|O_TRUNC, 0666
"a" O_WRONLY|O_CREAT|O_APPEND, 0666
"a+" O_RDWR|O_CREAT|O_APPEND, 0666
五、文件读写
ssize_t write(int fd, const void *buf, size_t count);
功能:写入文件内容
fd:文件描述符
buf:要写入的数据的内存首地址
count:要写入的字节数
返回值:成功写入的字节数
ssize_t read(int fd, void *buf, size_t count);
功能:读取文件内容
buf:存储数据的内存首地址
count:想读取的内存字节数
返回值:成功读取到的字节数
注意:它们与标准C的fwrite/fread很像,但更纯粹。
以二进制形式写入1000000个整数到文件中,分别用标准文件读写和系统文件读写来完成,比较它们谁的速度更快,为什么?
// 标准IO
#include <stdio.h>
int main(int argc,const char* argv[])
{
FILE* wfp = fopen("stdio.dat","w");
if(NULL == wfp)
{
perror("fopen");
return -1;
}
for(int i = 0; i<10000000; i++)
{
fwrite(&i,1,4,wfp);
}
fclose(wfp);
return 0;
}
/*
time ./stdio
real 0m0.224s
user 0m0.195s
sys 0m0.028s
*/
// 系统IO
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,const char* argv[])
{
int fd = open("sysio.dat",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(0 > fd)
{
perror("open");
return -1;
}
for(int i=0; i<1000000; i++)
{
write(fd,&i,4);
}
close(fd);
return 0;
}
/*
time ./sysio
real 0m1.137s
user 0m0.104s
sys 0m1.033s
*/
六、系统IO与标准IO
1、当系统调用被执行时,需要从用户态切换到内核态,执行完毕后再从内核态切换到用户态,频繁的切换就会导致性能损失。
2、标准IO在内部维护一个缓冲区(1k,1024字节),只有在满足特定条件才会把缓冲区中的数据调用write进入写入,因此降低了系统调用的使用频率,减少用户态和内核态的来回切换次数,因此标准IO的速度比系统IO更快
3、如果想提高系统IO的速度,可以尝试维护一个更大的缓冲区,先把数据存储在缓冲区中,等缓冲区满了,再调用write写入,这样系统IO会比标准IO更快。
4、普通情况建议使用标准IO,因为比直接使用系统IO要快,如果对速度有很高的要求,可以使用系统IO+大缓冲区。
// 系统IO+大缓冲区
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,const char* argv[])
{
int fd = open("sysio.dat",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(0 > fd)
{
perror("open");
return -1;
}
int buf[4096];
for(int i=0; i<1000000; i++)
{
buf[i%4096] = i;
if(i%4096 == 4095)
write(fd,buf,sizeof(buf));
}
close(fd);
return 0;
}
/*
time ./sysio
real 0m0.005s
user 0m0.000s
sys 0m0.005s
*/
5、UNIX和Linux只有这一套读写函数,没有文本文件的读写方式,可以使用ssanf/sprintf配合缓冲区来实现文本内容的读写。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct Student
{
int id;
char name[20];
float score;
}Student;
int main(int argc,const char* argv[])
{
int fd = open("text.txt",O_RDWR);
if(0 > fd)
{
perror("open");
return -1;
}
Student stu = {10012,"hehe",365};
char buf[256];
sprintf(buf,"%d %s %g\n",stu.id,stu.name,stu.score);
write(fd,buf,strlen(buf));
close(fd);
return 0;
}
使用系统IO实现带覆盖检查的cp命令。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,const char* argv[])
{
if(3 != argc)
{
puts("Use:./cp src dest");
return 0;
}
int src_fd = open(argv[1],O_RDONLY);
if(0 > src_fd)
{
perror("open src");
return -1;
}
int dest_fd = open(argv[2],O_WRONLY|O_CREAT|O_EXCL,0644);
if(0 > dest_fd)
{
printf("%s 文件已经存在是否覆盖?(y/n)",argv[2]);
char cmd = getchar();
if('y' != cmd && 'Y' != cmd)
{
close(src_fd);
printf("停止拷贝!\n");
return 0;
}
dest_fd = open(argv[2],O_WRONLY|O_TRUNC);
if(0 > dest_fd)
{
perror("open dest");
return -1;
}
}
int ret = 0;
char buf[4096];
while(ret = read(src_fd,buf,sizeof(buf)))
{
write(dest_fd,buf,ret);
}
close(src_fd);
close(dest_fd);
return 0;
}
七、文件位置指针
1、每个打开的文件都有一个记录读写位置的变量,它可能是整数,但习惯的称作位置指针,文件的读写操作都是从位置指针所指向地方进行的。
2、lseek可以设置文件的位置指针,与标准C不一样的是它的返回值是它调整后的位置指针,所以系统调用中没有与ftell对应的函数,因为lseek就包含fseek和ftell的功能。
off_t lseek(int fd, off_t offset, int whence);
功能:调整文件位置指针,用法与标准C的fseek基本一致。
offset:偏移值
whence:基础位置
SEEK_SET The offset is set to offset bytes.
SEEK_CUR The offset is set to its current location plus offset bytes.
SEEK_END The offset is set to the size of the file plus offset bytes.
3、在超过文件末尾的位置写入数据就会形成文件黑洞,黑洞不会占用磁盘空间,但会计算成文件的大小,而且也不会影响文件的读写。
使用lseek实现一个计算文件大小的命令,显示出文件的字节数,超过1024字节显示KB,超过1024KB显示MB,超过1024MB显示GB
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,const char* argv[])
{
if(2 != argc)
{
puts("Use:./filesize <name>");
return 0;
}
int fd = open(argv[1],O_RDONLY);
if(0 > fd)
{
printf("open %s %m\n",argv[1]);
return -1;
}
size_t bytes = lseek(fd,0,SEEK_END);
printf("%u\n",bytes);
if(bytes > 1024*1024*1024)
printf("%uGB %uMB %uKB %uByte\n",
bytes/1024/1024/1024,
bytes/1024/1024%1024,
bytes/1024%1024,
bytes%1024);
else if(bytes > 1024*1024)
printf("%uMB %uKB %uByte\n",
bytes/1024/1024 % 1024,
bytes/1024%1024,
bytes%1024);
else if(bytes > 1024)
printf("%uKB %uByte\n",
bytes/1024%1024,
bytes%1024);
else
printf("%uByte\n",bytes);
close(fd);
return 0;
}