一、C库函数(标准库函数)与系统函数区别
系统函数
主要是操作系统为用户设计的,用于应用程序进程和硬件设备(如CPU、磁盘、打印机等)之间进行交互提供的一系列接口API,说白了,就是应用程序和硬件设备之间的一个接口层。拿Linux来说,它是一个单内核OS,结构紧密,执行速度快,各个模块直接都是直接进行相互调用的,对于整个Linux操作系统,大致的 IO结构如下:
其中,系统调用接口位于Linux内核之中,再细分一下的话,又可以将Linux中的IO结构描述如下(用户进程->系统调用接口->linux内核子系统->硬件):
也就是说Linux内核包括了系统调用接口和内核子系统两部分;或者从下到上可以是(物理硬件->OS内核->OS服务->应用程序):
其中操作系统起到“承上启下”的关键作用,向下管理物理硬件,向上为操作系服务和应用程序提供接口,这里的接口就是系统调用了。
一般情况下,操作系统为了考虑管理的方便以及实现时的难度,它只提供一少部分的系统调用,这些系统调用一般都是由C和汇编混合编写实现的:
接口:C语言(方便上层调用)
具体实现:汇编语言(执行起来效率高)
库函数
库函数,顾名思义,就是把函数放到一个仓库里面。这里,库函数是将一个系列(应用)的程序源文件编译为一个或多个库文件(Linux下常见的:.lib(静态库)和.so(动态库)),将其提供给别人使用,别人在使用的时候,只需要包含这些库文件以及头文件即可,其中头文件中显示所有被提供出来的接口API,使用的时候,指定好库文件以及头文件路径,并将其头文件#include进工程即可。
库函数一般可分为两类,一类是随着操作系统提供的,叫做系统库函数,另一类是由第三方提供的,所以一般又叫做第三方库函数。
系统库函数把系统调用进行封装或者组合,可以实现更多的功能,这样的库函数能够实现一些对内核来说比较复杂的操作。比如,read()函数根据参数,直接就能读文件,而背后隐藏的比如文件在硬盘的哪个磁道,哪个扇区,加载到内存的哪个位置等等这些操作,程序员是不必关心的,这些操作里面自然也包含了系统调用。
而对于第三方的库,它其实和系统库一样,只是它直接利用系统调用的可能性要小一些,而是利用系统提供的API接口来实现功能(API的接口是开放的)。部分Libc库中的函数的功能的实现还是借助了系统调用,比如printf的实现最终还是调用了write这样的系统调用;而另一些则不会使用系统调用,比如strlen, strcat, memcpy等。
由于操作系统库函数使用时有很大的局限性,对于现实使用可操作性不强,但是它具有很强的可拓展性,基于此,慢慢地衍生出许多强大的第三方库,这些第三方库就是将众多的操作系统库函数API加以利用,针对不同的目的,转接成便于实际使用的API接口。其实,这样理解起来,基本上所有的第三方库归根结底都是通过调用系统库函数来实现实际功能的。
以存储器分配函数malloc为例。有多种方法可以进行存储器分配及与其相关的无用区收集操作(最佳适应,首次适应等),并不存在对所有程序都最佳的一种技术。如果我们不喜欢其操作方式,则我们可以定义自己的malloc函数,极 其可能,它还是要调用sbrk系统调用。
总而言之,系统调用是为了方便使用操作系统的接口,而库函数则是为了人们编程的方便。
【以上参考:https://blog.csdn.net/eleanor_12/article/details/53560830】
二、C标准库函数读写文件
首先C标准库函数是工作在系统库函数之上的。C标准库函数在读写文件时候都有一个文件流指针。【FILE*fp=NULL;// fp=fopen(F_PATH,”r”);】
FILE *是文件流指针,其指向结构体如下图所示:
1、每个FILE文件流都有一个缓冲区,默认大小是8192Byte.FILE结构体包含文件描述符,f_pos,buffer。
2、文件描述符指向磁盘文件,在进行文件读写操作时候是先读写到缓冲区,然后再调用系统应用层API write函数进行写操作,write将文件内容写到内核缓冲区,然后再调用内核层API sys_write进行写操作。到这样可以减少I/O操作,提高读写操作。
【值得说明的是:使用C语言标准库函数fopen()每打开一个文件时候,其都会对应一个单独一个缓冲区。 内核缓冲区是公用的。】
3、带缓冲区的文件IO会减少大量系统的I/O操作,可以提高程序运行效率。标准函数的I/O缓冲区只属于当前进程,而进程A/B都会通过调用底层(系统)的I/O,来读取或写入数据。对于系统I/O来说,它被进程A和进程B所共享。而标准I/O缓冲区只属于当前进程。
4、缓冲区刷新
刷新C标准缓冲区:缓冲区满、程序的正常结束、以及fclose操作,等都会刷新缓冲区。一般可以使用fflush()函数去刷新,值得说明的是:换行符\n 只能刷新终端文件的缓冲区。
刷新内核缓冲区:有个守护进程会定时刷新内核缓冲区。
下面以一个读写文件为例(流程图可见上图):
1. 将hello world字符写入磁盘hello.txt 文件中;
2.使用fopen打开hello.txt 文件,然后进行写;
3.写到C标准缓冲区;
4.满足刷新缓冲区条件,会调用系统应用层API write函数进行写操作;
5.write将文件内容写到内核缓冲区;
6.如果内核缓冲区没有满,系统不会立即调用内核层API sys_write将缓冲区内容写入到磁盘,有一个守护进程会定时刷新内核缓冲区;
7.此时有一个进程B读hello.txt这个文件,发现内核缓冲区就有这个文件内容,其就不需要访问hello.txt 磁盘文件了。
【以上参考: https://blog.csdn.net/huangshanchun/article/details/46388907】
三、PCB概念 (进程控制块)
进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志。
每个进程都会有两个和进程相关的结构体。
1、task_struct结构体 /usr/src/linux-headers-xxx/include/linux/sched.h
在这个结构体中有一个files_struct (也就是说在每个进程中都会有这样一个结构体)
2、用户空间是0-3G,内核空间是3-4G。
每个进程对应一个PCB,每个PCB内部有一个files_struct的结构体指针,通过这个结构体指针可以找到一个整形数组,而这个整形数组中的内容与系统的文件一一对应。我们使用fopen时,将会返回一个FILE指针,FILE指针中记录了一个文件的描述符,我们操作这个文件时根据这个文件描述符files_struct中整形数组的位置找到对应的文件。
注:当我们新打开一个文件时,所返回的文件描述符是在整形数组中未使用的最小的那一个。
四、文件IO操作函数
Unix系统可用的文件IO函数包括打开文件、读文件、写文件等等。
文件描述符
- 对于内核来说,所有打开文件都通过文件描述符引用。文件描述符是一个非负的整数,当打开一个现有的文件或者创建一个文件的时候,内核向进程返回一个文件描述符。当读写一个文件时,使用open或create返回文件描述符标识该文件,将其作为参数传给read或write。
POSIX标准程序中0,1,2分别表示标准输入,标准输出,标准错误相关联:
STDIN_FILENO 0 STDOUT_FILENO 1 STDERR_FILENO 2
- 新打开文件返回文件描述符表中未使用的最小文件描述符。文件描述符的变化范围为:0 ~ OPEN_MAX -1。该值存在于/proc/sys/fs/file_max文件下,使用cat命令查看该值可以得到当前系统允许打开的最大文件个数。
open、close
1、文件打开open
pathname参数:
该参数表示的是要要打开文件的文件路径。和fopen一样,pathname既可以是相对路径也可以是绝对路径。
flags参数:
这个flags参数必须包含O_RDONLY,O_WRONLY,O_RDWR三者中的一个。
以下可选项可以同时指定0个或多喝,和必选项按位或起来作为flags参数。
O_APPEND表示追加,如果文件已有内容,这次打开文件缩写的数据附加到文件的末尾而不覆盖原来的内容
O_CREAT若此文件不存在则创建它,使用此选项需要提供第三个参数model,表示该文件的访问权限
O_EXCL如果同时制定了O_CREAT,并且文件已存在,则出错返回。
O_TRUNC说明文件已存在,并且以可读可写,只读只写的方式打开,则将其长度截断为0字节。
O_NONBLOCK对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O。返回值:
成功返回新分配的文件描述符,出错返回-1并设置errno。
这里的文件描述符为3,因为在程序中默认打开了控制台输入,控制台输出,控制台出错输出三个文件描述符。注意:
int fd = open(“abc”,O_CREAT);//编译可以通过,但是我们查看文件时可以看到文件权限会出现垃圾信息。是因为当我们创建文件时,因为没有指定文件访问权限,程序会在内存中获取一个垃圾值做为文件的文件访问权限。
open() 和 C标准函数fopen()的区别
1、以可写的方式fopen一个文件时,如果文件不存在会自动创建,而open一个文件时必须明确指定O_CREAT才会创建文件,否则文件不存在就出错返回。
2、以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个文件时必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写。
2、文件关闭close
关闭函数比较简单,直接传入一个需要关闭的文件的文件描述符就可以了。
注意:
一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
【以上参考:https://blog.csdn.net/robin__chou/article/details/51470768】
read、write、lseek
1、文件读操作read
size_t类型和ssize_t类型:
ssize_t:表示有符号数
size_t:表示无符号数
参数说明:
fd:文件描述符
buf:为缓冲区首地址
count:为缓冲区大小(一般为sizeof(buf))
返回值说明:
成功返回读取到的字节数,
错误返回-1,并设置errno
2、文件写操作write
参数说明:
fd:文件描述符
buf:为存放数据的内存空间
count:为有效字符长度(一般为 strlen(buf))
3、文件偏移操作lseek
参数说明:
fd:文件描述符
offset:偏移量
whence:文件指针位置
- 关于文件指针位置whence:
SEEK_SET:设置文件指针基于文件开始偏移offset个字节
SEEK_CUR:设置文件指针基于当前位置偏移offset个字节
SEEK_END:设置文件指针在文件末尾偏移offset个字节
在一个进程启动时,会默认打开三个文件。标准输入,标准输出,标准出错。(0,1,2) 对应文件描述符的宏定义为
STDIN_FILENO 0
STDOUT_FILENO 1
STDERR_FILENO 2
在Linux中默认可以打开的文件个数为1024个,可以通过ulimit -a 查看:
我们可以通过命令ulimit -n 4096进行修改默认打开的最大文件数。但是在Linux系统中还存在一个最大打开文件数 cat /proc/sys/fs/file-max。
这个值与Linux所拥有的内存相关。
4、代码实践
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
//简单的文件读写
void testRdWr(int argCounter, char *filename)
{
if (argCounter < 2) {
printf("Usage: ./a.out Filename\n");
return;
}
int fd = open(filename, O_CREAT | O_APPEND | O_RDWR, 0666);
if (fd < 0) {
printf("Failed to open file: \"%s\"!\n", filename);
return;
}
char buffer[1024] = "Now, we are in testing testRdWr......\n";
int nW = write(fd, buffer, strlen(buffer));
memset(buffer, 0, sizeof(buffer));
lseek(fd, 0, SEEK_SET);
int len = read(fd, buffer, sizeof(buffer));
//输出到控制台
write(STDOUT_FILENO, buffer, len);
close(fd);
}
//通过循环拷贝文件
void loopCopy(int argCounter, char *fileSrc, char *fileDest)
{
int len;
char buffer[100];
if (argCounter < 3) {
printf("Usage: ./a.out Sourcefile Destnationfile\n");
return;
}
int fdSrc = open(fileSrc, O_RDONLY);
if (fdSrc < 0) {
perror("open source file\n");
return;
}
int fdDes = open(fileDest, O_WRONLY | O_APPEND | O_CREAT, 0666);
if (fdDes < 0) {
perror("open destnation file\n");
return;
}
again:
while (len = read(fileSrc, buffer, sizeof(buffer))) {
if (-1 == len) {
goto again;
}
write(fileDest, buffer, strlen(buffer));
bzero(buffer, sizeof(buffer));
}
close(fdSrc);
close(fileDest);
}
int main(int argc, char *argv[])
{
testRdWr(argc, argv[1]);
//loopCopy(argc, argv[1], argv[2]);
getchar();
return 0;
}
五、阻塞和非阻塞
读常规文件通常是不会阻塞的,不管读取多少个字节的内容,read都会在有限的时间内返回。
从终端或者网络读取数据时,如果终端输入不足一行,read一个终端设备就会阻塞。
如果设备没有读到数据就返回则返回值为-1,同时设置errno位为EAGAIN。
1、阻塞读
2、非阻塞读
- 注意:
在使用循环扫描时一定要使用延时,哪怕事件再紧急也要将少量的时间空闲出来。
O_NONBLOCK针对的是文件而不是read函数。 - 补充:
/dev/tty—终端
/dev/tty 就是类似于C++中的this 指针,表示的是当前的终端。
非阻塞等待读取数据时,我们同样需要添加等待超时处理,否则将出现死循环。
六、几种错误–errno/perror/strerror
- 解释:
perror:就是将errno解析成后面的注释并输出;
errno==EAGAIN,为阻塞被信号打断或者以非阻塞方式读取数据时出错返回后的标志,这时我们应该尝试再次去读取;
strerror:将错误号转换成对应的错误提示。
七、文件性质改变fcntl
—-可变参数(取决于cmd),类似于printf。
用fcntl改变File Status Flag
八、I/O通道管理ioctl
通过ioctl获取终端窗口大小:
关于ioctl请求的request参数以及所对应的arg地址必须指向的数据类型:
【ioctl()函数详解可参考:https://blog.csdn.net/shanshanpt/article/details/19897897】