一、系统调用
由操作系统实现并提供给外部应用程序的编程接口(Application Programming Interface,API),用户程序可以通过这个特殊接口来获得操作系统内核提供的服务
系统调用和库函数的区别:
系统调用(系统函数) 内核提供的函数
库调用 程序库中的函数
错误处理函数
errno用于记录系统的最后一次错误代码,返回一个int值(错误码),在errno.h中定义,不同的错误码表示不同的含义,新建errno.c如下:
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main(void)
{
FILE *fp = fopen("txt","r");//打开一个不存在的文件
if (NULL == fp)
{
printf("fopen failed\n");
printf("errno:%d\n",errno);//打印errno返回的错误码
printf("fopen:%s\n",strerror(errno));//使用strerror函数来解释错误码
return 1;
}
return 0;
}
编译再执行可得如下结果:
fopen failed
errno:2
fopen:No such file or directory
虚拟地址空间:
文件描述符:
- 当我们打开文件或者新建文件时,系统会返回一个文件描述符用来指定已打开的文件,这个文件描述符相当于这个已打开文件的标号,操作这个文件描述符就相当于操作这个描述符所指定的文件;
- 程序运行起来后每个进程都有一张文件描述符的表,标准输入、输出,标准错误输出,对应的文件描述符0、1、2就记录在表中,程序运行起来后这三个文件描述符是默认打开的;
文件描述符是指向一个文件结构体的指针
进程控制块(PCB):本质---结构体
FILE结构体:主要包含文件描述符、文件读写位置、IO缓冲区三部分内容
最大打开文件数:一个进程默认打开文件的个数1024
命令查看:ulimit -a 查看open files 对应值。默认为1024
可以使用ulimit -n 4096 修改
cat /proc/sys/fs/file-max可以查看该电脑最大可以打开的文件个数。受内存大小影响。
二、常用文件IO函数
1.open函数
#include <sys/types.h>
#include <sys.stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);参数:
pathname: 欲打开的文件路径名
flags:文件打开方式: #include <fcntl.h>
O_RDONLY|O_WRONLY|O_RDWR
O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK ....
mode:这个参数只有在文件不存在时有效,指新建文件时指定文件的权限
取值8进制数,用来描述文件的访问权限。 rwx 0664
创建文件最终权限 = mode & ~umask
返回值:
成功: 打开文件所得到对应的 文件描述符(整数)
失败: -1, 设置errno
flags必选项:
O_RDONLY 以只读的方式打开
O_WRONLY 以只写的方式的打开
O_RDRW 以可读、可写的方式打开
可选项,和必选项进行位或(|)
O_CREAT 文件不存在则创建文件,使用此选项时需使用mode说明文件的权限
O_EXCL 如果同时指定了O_CREAT,且文件已经存在,则出错
O_TRUNC 如果文件存在,则清空文件内容
O_APPEND 写文件时,数据添加到文件末尾
O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O
2.close函数
#include <unistd.h>
int close(int fd);
功能:
关闭已打开的文件
参数:
fd:文件描述符,open()的返回值
返回值:
成功:0
失败:-1,并设置errno
代码示例:
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
int main(int argc,char *argv[])
{
int fd;
open("./dict.cp",O_RDONLY | O_CREAT | O_TRUNC,0644);//rw-r--r--
printf("fd = %d\n",fd);
close(fd);
return 0;
}
3.write函数
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
功能:
把指定数目的数据写到文件(fd)
参数:
fd:文件描述符
buf:数据首地址
count:写入数据的长度(字节)
返回值:成功:实际写入数据的字节个数
失败:-1
4.read函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
功能:
把指定数目的数据读到内存(缓冲区)
参数:
fd:文件描述符
buf:内存首地址
count:读取的字节个数
返回值:
成功:实际读取到的字节个数
失败:-1
用read和write实现一个copy函数:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
int main(int argc,char *argv[])
{
char buf[1024];
int n = 0;
int fd1 = open(argv[1],O_RDONLY);//read
if (fd1 == -1){
perror("open argv1 error");
exit(1);
}
int fd2 =open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
if (fd2 == -1){
perror("open argv2 error");
exit(1);
}
while((n = read(fd1,buf,1024)) != 0){
if (n < 0){
perror("read error");
break;
}
write(fd2,buf,n);
}
close(fd1);
close(fd2);
return 0;
}
5.lseek函数
#include <sys/types.h>#include cunistd.h>
off_t 1seek(int fd,off_t offset,int whence);功能:
改变文件的偏移量(读写位置)参数:
fd:文件描述符offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸。
whence:其取值如下:
SEEK_SET:从文件开头移动offset个字节SEEK_CUR:从当前位置移动offset个字节
SEEK__END:从文件未尾移动offset个字节
返回值:
若1seek成功执行,则返回新的偏移量如果失败,返回-1
lseek允许超过文件结尾设置偏移量,文件会因此被拓展。
使用lseek获取文件大小:
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
int main(int argc,char *argv[])
{
int fd = open(argv[1],O_RDWR);
if (fd == -1)
{
perror("read error");
exit(1);
}
int lenth = lseek(fd,0,SEEK_END);//获取文件大小
//int lenth = lseek(fd,107,SEEK_END);//扩展文件大小
printf("file size:%d\n",lenth);
//write(fd,"a",1);
close(fd);
return 0;
}
应用场景:
1. 文件的“读”、“写”使用同一偏移位置。
2. 使用lseek获取文件大小(返回值接收)
3. 使用lseek拓展文件大小:要想使文件大小真正拓展,必须【引起IO操作】。
使用 trumcate 函数,直接拓展文件。
int ret =truncate("dict.cp”,250):
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
int main(int argc,char *argv[])
{
//open/lseek(fd,249.SEED_END)/write(fd,"\0",1);
int ret = truncate("dict.cp",250);
printf("ret = %d\n",ret);
return 0;
}
三、系统调用和库函数比较---预读入缓输出
fputc/fgetc实现:
int main(void){
FILE *fp,*fp_out;
int n = 0;
fp = fopen( " hello.c" , "r");
if(fp == NULL)
{
perror( " fopen error" );
exit( 1);
}
fp_out = fopen ( "hello.cp" ,"w" );
if(fp_out =NULL)
{
perror( "fopen error" );
exit(1);
}
while((n = fgetc(fp))!= EOF)
{
fputc(n, fp_out) ;
}
fclose(fp);
fclose(fp_out);
return 0;
}
read/write实现:
int main( int argc, char *argv[])
{
char buf[ 1];
int n = 0;
int fd1 = open(argv[1],0_RDONLY);
int fd2 = open(argv[2],O_RDWR|0_CREAT|0_TRUNC,0664);
while((n = read (fd1,buf,1)) != 0)
{
write(fd2, buf, n);
}
close(fd1);
close(fd2);
return 0;
}
结果表明:read/write速度慢
原因分析:
- read/write这块,每次写一个字节,会疯狂进行内核态和用户态的切换,所以非常耗时。
- fgetc/fputc,有个缓冲区,4096,所以它并不是一个字节一个字节地写,内核和用户切换就比较少
预读入,缓输出机制。所以系统函数并不是一定比库函数牛逼,能使用库函数的地方就使用库函数。
- 标准IO函数自带用户缓冲区,系统调用无用户级缓冲。系统缓冲区是都有的。
四、阻塞和非阻塞
产生阻塞的场景:读设备文件。读网络文件的属性。(读常规文件无阻塞概念)
/dev/tty -- 终端文件。
open("/dev/tty", O_RDWR | O_NONBLOCK) --- 设置 /dev/tty 非阻塞状态。(默认为阻塞状态)
更改非阻塞读取终端——超时设置
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"
int main(void)
{
//打开文件
int fd, n, i;
fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);
if(fd < 0){
perror("open /dev/tty");
exit(1);
}
printf("open /dev/tty ok... %d\n", fd);
//轮询读取
char buf[10];
for (i = 0; i < 5; i++){
n = read(fd, buf, 10);
if (n > 0) { //说明读到了东西
break;
}
if (errno != EAGAIN) { //EWOULDBLOCK
perror("read /dev/tty");
exit(1);
} else {
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
sleep(2);
}
}
//超时判断
if (i == 5) {
write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
} else {
write(STDOUT_FILENO, buf, n);
}
//关闭文件
close(fd);
return 0;
}
五、传入传出参数
传入参数:
1. 指针作为函数参数。
2. 同常有const关键字修饰。
3. 指针指向有效区域, 在函数内部做读操作。
传出参数:
1. 指针作为函数参数。
2. 在函数调用之前,指针指向的空间可以无意义,但必须有效。
3. 在函数内部,做写操作。
4。函数调用结束后,充当函数返回值。
传入传出参数:
1. 指针作为函数参数。
2. 在函数调用之前,指针指向的空间有实际意义。
3. 在函数内部,先做读操作,后做写操作。
4. 函数调用结束后,充当函数返回值。