1.系统调用
由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface,API)。是应用程序同系统之间数据交互的桥梁。
C标准函数和系统函数调用关系。一个helloworld如何打印到屏幕。
man手册中一共有九卷,其中一卷就有讲到系统调用,内核就当前操作系统的核心程序,系统的本质都是个程序。内核和硬件打交道,提供的函数只能给上层应用所使用。
提供的系统调用函数实际上在linux内核当中是没有的,只不过却有与之对应的一样功能的函数,比如open在内核当中的源码对应的是sys_open,虽然名字不同,但是几乎是一模一样的
sys_open >>浅封装>> open,操作系统避免与用户进行交互,但又不想让用户真正窥探到内核,因此使用了浅封装给内核中的sys_open包了个保护壳变成open函数让用户可以去调用系统而又不会导致让用户窥探到本质
2.open/close
函数原型:
要导入头文件 #include<unistd.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int close(int fd);
int creat(const char *pathname, mode_t mode);
# 可以通过 man open 2 指令去查看配置的文档
# pathname:文件路径
# flags:打开方式
# mode: 权限,mode_t是一个八进制的整型
# 返回的是一个文件描述符,如果执行错误,会返回-1
# 同时返回一个errno(操作系统的全局变量),需要导入 errno.h
# 返回的errno的数字想要知道是什么含义可以通过 man strerror 去查看
常用参数 -- flags
要导入头文件:#include<fcntl.h>
O_RDONLY | 读 |
O_WRONLY | 写 |
O_RDWR | 读写 |
O_APPEND | 追加 |
O_CREAT | 创建 |
O_EXCL | 存放 |
O_TRUNC | 截断 |
O_NONBLOCK | 非阻塞 |
创建文件时,指定文件访问权限。权限同时受umask影响。结论为:
文件权限 = mode & ~umask
使用头文件:<fcntl.h>
O_CREAT -- 如果没有该文件就进行创建
创建文件时,其权限与umask挂钩,比如umask = 0002,取反后就是775,然后与mode进行按位与(二进制),最后才得出创建文件的真正权限
O_TRUNC --- int ftruncate(int fd, off_t length); -- 把文件截断成0
open常见错误
1. 打开文件不存在
2. 以写方式打开只读文件(打开文件没有对应权限)
3. 以只写方式打开目录
perror、strerror、errno
运用
[....]# cd /linux_01
[....]# mkdir -a ./file_IO_test/test
[....]# cd ./file_IO_test/test
[....]# touch dict.txt
[....]# touch ./makefile
[....]# mkdir ./dirct
[....]# vim open.c
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
int main(int argc,char *agrv[])
{
int fd = 0;
fd = open("./dict.txt",O_RDONLY);
printf("fd = %d\n",fd);
int fd1 = 0;
fd1 = open("./dict.cp",O_RDONLY | O_CREAT,0644);
printf("fd1 = %d\n",fd);
//1.打开文件不存在
int fd2 = 0;
fd2 = open("./dict.cp1",O_RDONLY);
printf("fd2 = %d, error = %d:%s\n",fd2,error,strerror(error)));
//2.以写方式打开只读文件(打开文件没有对应权限)
int fd3 = 0;
open("./dict.cp2",O_RDONLY | O_CREAT,0411);
//创建一个只读文件
fd3 = open("./dict.cp2",O_WRONLY);
printf("fd3 = %d, error = %d:%s\n",fd3,error,strerror(error)));
//fd = -1,errno=13:Permissiondenied
//3.以只写方式打开目录
int fd4 = 0;
fd4 = open("./dict.cp2",O_WRONLY);
printf("fd4 = %d, error = %d:%s\n",fd4,error,strerror(error)));
// fd = -1, errno=21:Is a directory
close(fd);
......
return 0;
}
3.read/write函数
ssize_t read(int fd, void *buf, size_t count);
从指定位置fd读,然后将读取的东西存入缓冲区buf(待写出数据的缓冲区),count是数据的大小
成功的话会返回读取到的字节数(读取到文件尾部会返回0),失败的话会返回-1,同时设置errno
ssize_t write(int fd, const void *buf, size_t count);
练习:编写程序实现简单的cp功能。
[....]# vim cptest.c
#include<Stdio.h>
#include<stdlib.h> //perror所用到的头文件
....
#include<fcntl.h>
int main(int agrc,int **argv){
char buf[1024];
int n = 0;
int fd1 = open(argv[1],O_RDONLY); //read
if(fd1 == -1){
perror("open argv1 error\n");
// 如果读取出错就打印出自定义提示
exit(1);
}
int fd2 = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
# fd2是创建打开一个文件进行进行写入操作,如果该文件中有我内容则清0
# 如果没有该文件就创建该文件,目的是将fd1的内容写入到fd2
if(fd2 == -1){
perror("open argv2 error\n");
// 如果读取出错就打印出自定义提示
exit(1);
}
//写入操作:
while((n = read(fd1,buf,1024))!=0){
if(n < 0){
perror("read error");
break;
}
write(fd2,buf,n);
// 第三个参数为n是为了防止缓冲区资源的浪费
}
close(fd1);
close(fd2);
return 0;
}
gcc cptest.c -o cptest
./cptest.c open.c open2.c
# open.c是要拷贝过去的内容,open2.c是拷贝目的地
比较:如果一个只读一个字节实现文件拷贝,使用read、write效率高,还是使用对应的标库函数(fgetc、fputc)效率高呢?
理想上是write比fputc效率更高:
不管是fputc还是write,最终目的都是要从用户空间进入到系统内核,然后系统内核去驱动磁盘进行写入写出工作,但是只有系统函数才能才去调用系统内核让驱动工作,因此write效率更高,因为write本身就是系统调用提供的函数。而fputc并不是系统调用提供的函数,它底层是先去调用write,然后从用户空间进入到内核空间,借助驱动去驱动磁盘工作
但真是这样吗?
但实际上却是fputc比wirte效率更快:
strace ./可执行文件名称
可以看函数的调用过程
用read和write:read一次读一个,write一次写一个,一次只读取一个字节
用fgetc和fputc:并不是一个字节一个字节的操作,而是4096个字节的进行一次操作
探究:
fputc有自己的缓冲区,当缓冲区的东西满了后,才会去调用write进入到内核,将数据放入到系统级缓冲区,调用驱动进入到磁盘
而write和read没有自己的缓冲区,又因为人为的将buf缓冲区设置为1,因此只能不断地来回反复操作只读取或写入一个字节
----因此可以将buf缓冲区的大小重新设置好,就能实现wirte和read效率更高
read、write函数常常被称为Unbuffered I/O。指的是无用户及缓冲区。但不保证不使用内核缓冲区。-----预读入和缓输出
4.文件描述符
PCB进程控制块
使用命令查看其位置:
[....]# locate sched.h
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h
PCB进程控制块就像一个结构体
struct task_struct { 结构体 }
文件描述符
结构体PCB 的成员变量file_struct *file 指向文件描述符表。
从应用程序使用角度,该指针可理解记忆成一个字符指针数组,下标0/1/2/3/4...找到文件结构体。
本质是一个键值对0、1、2...都分别对应具体地址。但键值对使用的特性是自动映射,我们只操作键不直接使用值。
value中就是个指针指向了一个文件结构体,这个文件结构体中记录了进行操作文件的内容,比如属组、属主、路径等,但操作系统不想暴露这些value,因此只返回了key的值 -- 文件描述符
新打开文件返回文件描述符表中未使用的最小文件描述符。
- STDIN_FILENO 0
- STDOUT_FILENO 1
- STDERR_FILENO 2
最大打开文件数:
一个进程默认打开文件的个数1024。
命令:ulimit -a 查看open files 对应值。默认为1024,可以使用ulimit -n 4096 修改;当然也可以通过修改系统配置文件永久修改该值,但是不建议这样操作。
cat /proc/sys/fs/file-max
可以查看该电脑最大可以打开的文件个数。受内存大小影响
FILE结构体
主要包含文件描述符、文件读写位置、IO缓冲区三部分内容。
struct file {
...
文件的偏移量;
文件的访问权限;
文件的打开标志;
文件内核缓冲区的首地址;
struct operations * f_op;
...
};
查看方法:
(1) /usr/src/linux-headers-3.16.0-30/include/linux/fs.h
(2) lxr:百度 lxr → lxr.oss.org.cn → 选择内核版本(如3.10) → 点击File Search进行搜索
→ 关键字:“include/linux/fs.h” → Ctrl+F 查找 “struct file {”
→ 得到文件内核中结构体定义
→ “struct file_operations”文件内容操作函数指针
→ “struct inode_operations”文件属性操作函数指针
5.阻塞和非阻塞
读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:
正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。
- 阻塞读终端:【block_readtty.c】
- 非阻塞读终端【nonblock_readtty.c】
- 非阻塞读终端和等待超时【nonblock_timeout.c】
注意,阻塞与非阻塞是对于文件而言的。而不是read、write等的属性。read终端,默认阻塞读。
总结read 函数返回值:
- 返回非零值: 实际read到的字节数
- 返回-1:
- errno != EAGAIN (或!= EWOULDBLOCK) read出错
- errno == EAGAIN (或== EWOULDBLOCK) 设置了非阻塞读,并且没有数据到达。read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据
- 返回0:读到文件末尾
例子:
/dev/tty -- 终端文件,STDIN_FILENO和STDOUT_FILENO都是终端文件中的内容,实际上终端文件是已经跟着打开的了
标准输入和输出,当运行该文件时,如果不按键盘输入东西和点击回车,这时候终端就在等待,称为阻塞
阻塞是设备文件、网络文件的属性,不要误以为阻塞是read和write的特性,并且读常规文件无阻塞属性
那么能不能用非阻塞的方式去操作终端文件?
用open中的O_NONBLOCK非阻塞的方式打开tty终端文件,这时候只会反复的读,读到了就输出到屏幕,读不到就继续读,终端并不会进入等待状态(阻塞)
6.fcntl函数
【fcntl.c】 想改变文件的访问控制属性,比如从阻塞状态变成非阻塞状态,那就得关闭文件重新打开文件进行操作,而使用 fcntl 函数可以直接改变
改变一个【已经打开】的文件的访问控制属性,重点掌握这两个参数的使用,F_GETEL 和 F_SETFL
F_GETFL 读取到标准输入终端文件的状态后,通过 |= 添加上非阻塞属性,然后用F_SETFL 将其重新设置,使标准输入变为非阻塞
7.lseek函数
理解
官方定义:重新定位读或写的文件偏移量。
Linux中可使用系统函数lseek来修改文件偏移量(读写位置)
- 每当打开一个文件,都会有一个叫做“当前文件偏移量”的东西,如果难理解也可以将他理解为指针。 除非打开文件时指定O_APPEND选项,否则文件偏移量默认设置为0。当我们发生了一次读或者写操作时,都会使这个当前文件偏移量发生变化,读/写多少字节,当前偏移量就会向后移动多少。
- 因此当我们对一个新文件进行完写操作后,进行读操作,会发现什么都读不到,是因为偏移量经过写操作后移到了文件尾部,此时进行读操作肯定什么都读不到了,也就是读和写操作用的是同一个偏移量(文件指针)
lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。
回忆fseek的作用及常用参数:
SEEK_SET、SEEK_CUR、SEEK_END
int fseek(FILE *stream, long offset, int whence);
成功返回0;失败返回-1
特别的:超出文件末尾位置返回0;往回超出文件头位置,返回-1
include<lseek.c>
off_t lseek(int fd, off_t offset, int whence);
参数:fd //文件描述符,可以通过open函数得到,通过这个fd可以操作某个文件
参数: offset //文件偏移量,是一个整形数,与whence对应的位置继续往后偏移
参数:whence //偏移类型,下列三个值中选一个。
SEEK_SET:该文件的偏移量设为离文件开头offset个字节.
SEEK_CUR:该文件的偏移量设为其当前值加offset(PS :offest可正负).
SEEK_END:该文件的偏移量设为文件长度加offset
特别的:lseek允许超过文件结尾设置偏移量,文件会因此被拓展。
注意文件“读”和“写”使用同一偏移位置。
lseek函数返回的偏移量(off_t)总是相对于文件头而言的。
作用
- 移动文件指针到文件头:
lseek(fd, 0, SEEK_SET)
- 获取当前文件指针的位置:
lseek(fd, 0, SEEK_SUR)
- 获取文件长度:
lseek(fd, 0, SEEK_END)
- 拓展文件的长度,当前文件10b、110b,增加了100个字节:
lseed(fd, 100, SEEK_END)
,需要注意拓展完需要再写一次数据,否侧拓展无效
还有另一种方式也可以来拓展文件的大小
[....]# main 2truncate # 可以查找到另一个函数truncate
>>>int truncate(const char* path, off_t length)
例子
1.移动文件指针到文件头和获取当前文件指针的位置
//导入所有需要的头文件
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
//我们的目的是:移动文件指针到文件头:
int main()
{
//获取文件的文件描述符
int fd = open("text.txt", O_RDWR);
if (fd == -1)
{
perror("open");
return -1;
}
//输出当前文件的偏移量
long long int loc = lseek(fd, 0, SEEK_CUR);
printf("%lld\n", loc); //0
//使用read函数读3个字节的数据
char buf[3] = {0};
int rnum = read(fd, buf, sizeof(buf));
printf("%d\n", rnum); //3
//再次查看文件的偏移量(获取当前文件指针的位置)
long long int loc1 = lseek(fd, 0, SEEK_CUR);
printf("%lld\n", loc1); //3
//移动文件指针到文件头
long long int loc2 = lseek(fd, 0, SEEK_SET);
printf("%lld\n", loc2); //0
return 0;
}
2.获取文件长度
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
int main()
{
//获取文件的文件描述符
int fd = open("hello.txt", O_RDWR);
//获取文件长度
long long int loc1 = lseek(fd, 0, SEEK_END);
printf("%lld\n", loc1);
return 0;
}
3.拓展文件的长度(注:拓展完成后需要再写一次数据,否则拓展无效)
int main()
{
//获取文件的文件描述符
int fd = open("hello.txt", O_RDWR);
//获取文件长度
long long int loc1 = lseek(fd, 0, SEEK_END);
printf("%lld\n", loc1);
//拓展文件的长度,需要引起IO操作后文件的大小才会改变
//可以通过 ll 指令查看文件的详细属性
long long int loc2 = lseek(fd, 100, SEEK_END);
write(fd," ",1);//写入一个空数据
//该空数据前的100个字节是文件空洞,这是系统自动帮我们填补的:^@
printf("%lld\n", loc2);
return 0;
}
od -tcx filename 查看文件的16进制表示形式
od -tcd filename 查看文件的10进制表示形式
8.传入传出参数
传入参数:
1.指针作为函数参数
2.通常有const关键字修饰
3.指针指向有效区域, 在函数内部做读操作
char *strcpy(cnost char *src, char *dst);
传出参数:
1.指针作为函数参数
2.在函数调用之前,指针指向的空间可以无意义,但必须有效(如不能指向未初始化的空间)
3.在函数内部,做写操作
4.函数调用结束后,充当函数返回值
传入传出参数:
1.指针作为函数参数
2.在函数调用之前,指针指向的空间有实际意义
3.在函数内部,先做读操作,后做写操作
4.函数调用结束后,充当函数返回值
char strtok(char str, const char delim, char ** saveptr)
其中第三个参数就是传入传出参数