原文:
Linux系统编程的主要内容,就是分门别类的讲解Linux操作系统各个部份的原理,之后介绍或展示相关的系统调用API函数。
这一部份的内容十分多,几乎牵扯到了从第1章开始以来的大部章节中所提到的概念。所以要分三部份讲解。这儿是最为基础的A部份。
1系统调用
我们再度回顾一下系统调用的概念。
一个系统调用的流程
系统调用,中文名叫“systemcall”,它是操作系统内核里的一些内建的函数库,不光Linux平台上有,Windows、Andoris、IOS,以及华为新出的鸿蒙上也有。这种函数可以拿来完成一些系统系统调用把应用程序的恳求传给内核,内核再调用更底层的内核函数完成所需的处理,并将处理结果返回给应用程序。这种函数集合上去就称作程序插口或应用编程插口(ApplicationProgrammingInterface,API)。
把内核与应用程序分开,是例如Linux、UNIX、Andorid之类操作系统大展神威的基础,也是操作系统发展的必然。我们要在这个系统上编撰各类应用程序,就得通过这个API插口来调用系统内核上面的函数。这也是操作系统一惯的通行做法。若果没有系统调用API和内核函数,这么应用程序就丧失内核的支持linux操作系统培训,用户程序开发将寸步难行,编撰小型应用程序更是天方夜谭!
关于系统调用的详尽解释,请参看我的《Linux系统编程003-系统调用、API、标准C库》。
2I/O-输入/输出
I/O就是input/output,英文意思就是输入/输出。人们为了防止麻烦,往往省去中间的“/”,直接写作IO。
假如你以动作为视角,Input就是讲到文件中,Output就是从文件中读出。假如你以文件为视角,IO就是写文件、读文件的意思,我们英文通常说“读写”,而不是“写读”,所以IO的英文概念说上去就是:读写文件。
在inux上,正由于一切皆文件,所以对文件的IO操作无处不在。不论是shell命令处理文件、chrome上网、还是smplayer娱乐看影片,背后无一不是在读写文件、进行IO操作。
Linux操作系统提供了两种途径,供你对文件进行IO读写。一种是SHELL命令和常用的工具软件。另外一种方式就是自已编撰程序。本文主要是从编程角度来述说一系列与IO相关的话题。
虽然shell命令的本身linux虚拟主机,不仅内部命令-内核本身提供的命令之外,大多也是深度调用系统API来实现的功能。例如你想自已动手来一个自已的类似于cp命令的程序,十分简单,但前提是,得了解文件IO、stdio库IO,还要清楚缓冲、文件系统inode、文件描述符等这种基本的概念才行。(假如还不了解的,请参考我的《Linuxshell命令:管线操作的深度理解和代码实证》、《Linux系统编程笔记-文件描述符》、《Linux系统编程学习004-文件描述符、文件IO、C库IO》等前期文档。)
3文件IO与stdio库IO
既然Linux中一切皆文件,这么对文件的基本编程操作-IO操作,是我们绕不开的话题。编撰程序有两套函数可供使用,系统API提供的IO函数、或者标准C库的IO函数来对文件进行读写。
(1)文件IO。Linux系统提供了
open/read/write/fcntl/dup/lseek/close等系统IO函数。那些文件IO函数是系统API函数的一部份,是供面向底层开发、进行系统编程而调用的函数集。
文件IO函数是Linux操作系统提供的底层API函数,它没有通用性。例如windows里面类似的API函数是:
CreateFile/WriteFile/ReadFIle/DeleteFile/GetFileSize等。其实,windows下边的文件概念windows是面向设备的,不同的设备可能API都不相同。这与Linux里面一切皆文件有着本质的不同。
(2)标准IO。由标准C库提供的
fopen/fread/fwrite/fseek/ftell/fclose/printf等sdio库函数,称作之为标准C函数库。这种函数是在文件IO函数的基础上进行了封装,是面下层应用开发、进行应用编程而调用的函数集。
例如:printf(string,format)函数的输出,虽然相当于系统IO的write(1,formated_string)。
标准C库函数是通用的,当前主流操作系统都支持,可以跨本台使用。就是华为鸿蒙OS下来了一定也得无条件支持。
4标准IO编程示例
标准IO是对文件IO的封装,由于面向下层应用开发,使用上简单,特别容易上手。但是,一些太容易到手的东西,都是圈套,请看下例:
#include
#include
void stdio_printf_test(void)
{
printf("This is a samplen");
for(int i=0;i<10;++i)
{
printf("wait %d seconds, ", i);
sleep(1);
}
}
int main(int argc,char *argv[])
{
stdio_printf_test(void);
return 0;
}
里面一段简单代码目的是:先输出“Thisisasample”,之后每隔1秒复印“wait1second,wait2sencods......。但是,测试的结果是:先输出“Thisisasample”,之后等待10秒以后,哗啦一下,把wait1seconds、wait2seconds到wait10seconds一下子输出来。
$ gcc main.c
$ ./a.out
This is a sample
wait 1 seconds, wait 2 seconds, wait 3 seconds, wait 4 seconds, wait 5 seconds,wait 6 seconds, wait 7 seconds, wait 8 seconds, wait 9 seconds,wait 10 seconds,
测试结果表明,代码运行结果不正确,不像我们意料的那样简单。标准IO调用,容易形成问题,这么文件IO呢?下边针对文件IO调用再试一下。
5文件IO编程示例
月球人都晓得,文件IO编程的通用步骤只有三板斧:
#include
#include
#include
#include
#include
void fileio_write_to_file ( int argc,char *argv[] )
{
char read_buf[64];
ssize_t read_len = 0;
ssize_t write_len = 0;
//argc,argv判断部分略
int fd = open( argv[1],O_RDWR|O_CREAT,S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);/*rw-rw-rw-*/
if ( fd < 0 ) { perror("open"); return ; }
write_len = write( fd, "This is a sample" ,16 );
if ( write_len < 0) { perror("write"); return ; }
//lseek(fd,0,SEEK_SET);
read_len = read(fd , read_buf , 16);
if ( read_len < 0 ) { perror("read"); return ; }
printf("write %ld bytes , read %ld bytes, data : %sn",write_len, read_len ,read_buf);
if ( close(fd)) { perror("close"); }
}
int main(int argc,char *argv[])
{
fileio_write_to_file(argc,argv);
return 0;
}
编译里面的示例代码,运行以后的结果如下:
$ gcc main.c
$ ./a.out abc.txt
write 17 bytes , read 0 bytes, data :
$ cat abc.txt
This is a sample
write_to_file()函数的目的是对文件“abc.txt”写入"Thisisasample",之后再读下来。可惜,事与愿违linux编程 系统io,啥也没有读下来。而且你真的去读一下abc.txt的内容"catabc.txt",发觉write写入是正确的。这么,虽然文件IO操作简单到只有三板斧,而且里面的简单事例write写入正确linux编程 系统io,而read却又不正确。
6缓冲问题
如此简单的两个测试小反例,测试标准IO的stdio_printf_test()和测试文件IO的fileio_write_to_file(),均不能正常运行。
我们仔细观察测试输出,虽不能如愿,但结果也有部份的正确:stdio_printf_test()函数最终向终端输出了所有的字符串,fileio_write_to_file()也最终把“Thisisasample”写入了文件abc.txt。
问题出在缓冲上。标准stdio库在操作c盘文件时,通常是先把数据缓冲上去,直至缓冲区塞满,或则达到指定的条件,才能调用文件IO进行实际上的写入动作。所以人们把标准IO称为“bufferedIO”-带缓存的IO。
与此对应,文件IO就是无缓存的IO(unbufferedIO)。把stdio_printf_test()函数改成文件IO的方法,“wait1second,wait2sencods......“便可以次序输出了。更改后的fileio_printf_test()代码如下:
void fileio_printf_test(void)
{
write(1,"This is a samplen",17);
char buf[32];
for(int i=0;i<10;++i)
{
sprintf(buf,"wait %d seconds, ", i);
write(1,buf,strlen(buf));
sleep(1);
}
}
7标准I/O缓冲类型
标准I/O库中的stream(即FILE结构的流文件)提供了三种类型的stdio缓冲:
全缓冲(fullybuffered):这些缓冲模式下,只有在stdio缓冲区被塞满后才能进行实际的IO操作(即调用read/write系统IO),也就是说单次读、写数据的大小与stdio缓冲区大小相同。一般打开的文件流是全缓冲的(文件坐落c盘上,而c盘是块设备)。
行缓冲(linebuffered):这些缓冲模式下,当在输入和输出流遇见换行符时,标准I/O库执行I/O操作(即调用read()或则write()系统调用)。一般情况下,stdin和stdout都是涉及的按键显示器这种字符设备,所以是行缓冲的。
无缓冲(unbuffered):这些缓冲模式挺好理解,就是不存在stdio缓冲区,每次I/O操作就直接调用read()/write()系统IO。stderr一般是不带缓冲的,这就促使出错信息可以早日显示下来,而不管它们是否富含一个换行符。
正由于stdout是行缓冲,所以上面的反例stdio_printf_test()中,”printf("Thisisasamplen")“中有换行符”n“,所以先输出。前面的“printf("wait%dseconds,",i)”没有换行符,缓冲也没有塞满,到最后才把所有内容一次性地显示下来。
我们可以调用以下几个函数来显示地改变stdio缓冲的模式。
#include
void setbuf(FILE *stream,char *buf);
void setbuffer(FILE *stream, char *buf, size_t size);
void setlinebuf(FILE * stream);
int setvbuf(FILE *stream, char *buf,int mode , size_t size);
不仅可以调用上述几个函数可以显示地改变stdio缓冲的模式,我们还可以直接强制新刷新stdio缓冲区:调用fflush()函数可以立即将stdio缓冲中的内容写入文件中去。
站在内核的角度来看,所谓flush的本质:就是立刻调用无缓冲的wirte()强制stdio缓冲区中的数据写入内核高速缓冲区。
明白了以上的道理,stdio_printf_test()做以下改动就可以:要么通过“setvbuf(stdout,NULL,_IONBF,0);”把标准输出置为无缓冲,要么每次prinf()然后主动刷新“fflush(stdout);“。
#include
#include
void stdio_printf_test(void)
{
printf("This is a samplen");
#ifdef USE_NO_BUFFERED
setvbuf(stdout,NULL,_IONBF,0) ; //对其返回值的判断略去
#endif
for(int i=0;i<10;++i)
{
printf("wait %d seconds, ", i);
#ifdef USE_FLUSH
fflush(stdout); //
#endif
sleep(1);
}
}
int main(int argc,char *argv[])
{
stdio_printf_test(void);
return 0;
}
下边有更好的测试代码,可以便捷的测试stdio的三种缓冲模式和强制刷新模式,只要用相对应的宏编译即可。
#ifdef USE_LINE_BUFFERED
char static_line_buf[50]; //行缓冲,满50个字节就输出,也就是满20字节底层会write到文件
#endif
#ifdef USE_FULLY_BUFFERED
char static_full_buf[500]; //全缓冲,满500个字节就输出,也就是满500字节底层会write到文件
#endif
void stdio_write_file_test(void)
{
FILE * fp;
char buf[32];
fp = fopen(“abc.txt”,"w+");
#ifdef USE_NO_BUFFERED
setvbuf(fp,NULL,_IONBF,0) ; //对其返回值的判断略去
#endif
#ifdef USE_LINE_BUFFERED
setvbuf(fp,static_line_buf,_IOLBF,sizeof(static_line_buf)); //对其返回值的判断略去
#endif
#ifdef USE_FULLY_BUFFERED
setvbuf(fp,static_full_buf,_IOLBF,sizeof(static_full_buf)); //对其返回值的判断略去
#endif
fwrite("This is a samplen",17,1,fp);
for(int i=0;i<20;++i)
{
sprintf(buf,"wait %d seconds,", i);
fwrite(buf,strlen(buf),1,fp);
#ifdef USE_FLUSH
fflush(fp);
#endif
sleep(1);
}
//fclose(fp);
}
int main(int argc,char *argv[])
{
stdio_write_file_test();
return 0;
}
编译:
无缓冲测试: gcc main.c -o no_buf_test.out -D USE_NO_BUFFERED
行缓冲测试: gcc main.c -o line_buf_test.out -D USE_LINE_BUFFERED
全缓冲测试: gcc main.c -o full_buf_test.out -D USE_FULLY_BUFFERED
强制刷新测试:gcc main.c -o flush_buf_test.out -D USE_FLUSH
测试方式:开启两个终端,一个拿来观测执行命令“tail-Fabc.txt”,另一个就拿来依次执行前面的输出程序no_buf_test.out、line_buf_test.out、full_buf_test.out或flush_buf_test.out。
其实,里面的示例代码编译后,测试上须要开两个终端,运行两个进程去测试。假如你嫌这样做麻烦,可以对标准输出stdout进行同样的测试。
#include
#include
#include
#include
#include
#ifdef USE_LINE_BUFFERED
char static_line_buf[50]; //行缓冲,满50个字节就输出,也就是满20字节底层会write到文件
#endif
#ifdef USE_FULLY_BUFFERED
char static_full_buf[500]; //全缓冲,满500个字节就输出,也就是满500字节底层会write到文件
#endif
void stdio_print_test(void)
{
#ifdef USE_NO_BUFFERED
setvbuf(stdout,NULL,_IONBF,0); //对其返回值的判断省略
#endif
#ifdef USE_LINE_BUFFERED
setvbuf(stdout,static_line_buf,_IOLBF,sizeof(static_line_buf)); //对其返回值的判断省略
#endif
#ifdef USE_FULLY_BUFFERED
setvbuf(stdout,static_full_buf,_IOFBF,sizeof(static_full_buf)); //对其返回值的判断省略
#endif
printf("This is a samplen");
for(int i=0;i<10;++i)
{
printf("wait %d seconds,", i);
#ifdef USE_FLUSH //强制刷新
fflush(stdout);
#endif
sleep(1);
}
}
int main(int argc,char *argv[])
{
stdio_print_test();
return 0;
}
可以像上个类库那样进行编译:
无缓冲测试: gcc main.c -o stdout_no_buf_test.out -D USE_NO_BUFFERED
行缓冲测试: gcc main.c -o stdout_line_buf_test.out -D USE_LINE_BUFFERED
全缓冲测试: gcc main.c -o stdout_full_buf_test.out -D USE_FULLY_BUFFERED
强制刷新测试:gcc main.c -o stdout_flush_buf_test.out -D USE_FLUSH
由于是讲到屏幕里面的,所以不用tail命令去跟踪,分别运行stdout_no_buf_test.out、stdout_line_buf_test.out等,立刻可以看各类缓冲模式或刷新疗效。
8标准IO行缓冲flush刷新条件
遇见下边五种情况,行缓冲都会被刷新。
不仅上述5条之外,《Linux/Unix系统编程指南》告诉我们:在包括GLIBC库在内的许多C函数库实现中,若stdin和stdout指向终端,这么无论何时从stdin中读取输入时,都将蕴涵调用一次fflush(stdout)函数,这将刷新写入stdout的任何数据。之后这并不是标准,要保证程序的可移植性,应当显示地调用fflush(stdout)。
9标准IO全缓冲flush刷新条件
下边三种情况,全缓冲会被刷新:
其实,假如须要及时刷新,不论是全缓冲还是行缓冲,自动调用fflush函数,是最为稳当的方式。
为何进程结束时调用exit()时,在缓冲区中的内容也会被刷新呢?那是由于,调用exit()函数会执行一些释放进程资源的动作,其中就包括了关掉所有标准IO流。所以,stdio_write_file_test()函数的最后,尽管fclose(fp)被注释掉了,可还是能写成功。(其实,这样做是不可取的,最好还是显示的fclose(fp)。)
10怎样晓得标准IO-stdout的行缓冲大小?
读C库源码可以晓得。其实,假如你没有这份闲工夫的话,下边一段代码是可以测试下来的。
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int num = atoi(argv[1]); //命令行要输入写入的字节数
for (int i = 0;i < num ; ++i)
{
printf("a");
}
sleep(5);
return 0;
}
输入命令加上“字节数”参数,例如"./a.out1000",或"./a.out1024"。当你发觉1024字节以下,都需等5秒就能显示下来,而过了1024字节以后,是先把上面1024字节的内容显示下来,再延时5秒,之后再把剩余的内容显示下来。这就说明标准C库的行缓冲是1024字节。其实,里面的main测试函数也可以像下边这样简单。
int main(int argc,char *argv[])
{
int num = atoi(argv[1]);
fwrite("a",1,num,stdout);
sleep(5);
return 0;