1. 实现间隔时间横向打印 * * * * * * 字符串效果
很容易写出代码:
int main(void)
{
int n = 6;
while (n--)
{
printf("*");
usleep(100000);
}
printf("\n");
return 0;
}
发现运行结果并非是间隔100ms打印一个 * 而是等到600ms超时后将6个 * 一起打印出来。这是输出缓冲区是行缓冲的缘故,代码若改成:
while (n--)
{
printf("*\n");
usleep(100000);
}
这样就可以间隔100ms打印一个 * ,但是纵向打印显然不是我们要的效果。printf是往标准输出打印数据的,它会先将数据放到缓冲区,直到遇到换行符才将数据往标准输出打印。但是我们并不需要换行符,这就需要在代码上手动刷新输出缓冲区。
while (n--)
{
printf("*\n");
fflush(stdout); //刷新输出缓冲区,使得该缓冲区上的 数据得以打印到终端
usleep(100000);
}
这样就可以实现要求的效果了。
2. 实现菜单显示及相应处理
显示菜单实现函数:
void show_menu()
{
printf("------ 1. Linux ------\n");
printf("------ 2. Windows ------\n");
printf("------ 3. Mac OS ------\n");
printf("please input:[1 - 3]: ");
}
main函数:
int main(void)
{
int n;
int quit = 0;
while (!quit)
{
show_menu();
scanf("%d", &n);
switch(n)
{
case 1:
printf("\nos: Linux\n\n");
break;
case 2:
printf("\nos: Windows\n\n");
break;
case 3:
printf("\nos: Mac OS\n\n");
break;
case -1:
quit = 1;
break;
default:
break;
}
}
return 0;
}
如上程序若键入数字可以正确运行,但是键入非数字的字符,程序便陷入循环打印中。
scanf()和getchar()函数都是从标准输入中去获取数据,前者获取成功返回获取到的数据个数,失败返回0;后者获取一个字符,获取成功返回该字符,失败返回-1。程序之所以会陷入循环打印中,是因为标准输入缓冲区的缘故:scanf函数会先从输入缓存区获取数据,假设输入缓存区数据为空就会去终端陷入阻塞获取,反之就去读缓存区上的数据。在这它要获取的是int型的数据,假设程序使用者键入的是非数字的字符,那么scanf获取数据失败,缓存区上的非数字数据会一直遗留(若是数字数据会被读取,缓存区就不存在该数据),程序会往下执行。下次调用scanf时候,同理会先去判断缓冲区上的数据,缓冲区有数据,所以不会使得终端阻塞等待输入,但该数据又不是int型数据,依次获取失败,程序又会接下往下执行…因此便陷入循环了。解决办法就是不管scanf()函数获取成功与否,都把输入缓存区的数据清空。
scanf("%d", &n);
while (getchar() != '\n'); //目的在于清空输入缓冲区
3. stdin、stdout、stderr对应的文件描述符
I/O 缓冲区对应的设备文件是/dev/stdin和/dev/stdout,它们分别被称之为标准输入和标准输出,另外还用一个标准错误输出,设备文件为/dev/stderr:
在Linux启动时候会启动bash进程,该进程会默认打开着3个设备文件。由于Linux上最小的文件描述符是从0开始,且打开文件时 返回最小可用文件描述符,所以它们的文件描述符分别为0、1、2。
既然知道了文件的描述符,那么自然可以调用系统调用open、write来读写文件了:
int main(void)
{
int ret;
int i;
char buf[1024] = {};
//往标准输出缓冲区写入数据
write(1, "hello\n", 6); //在终端打印"hello\n"
//读取标准输入缓存区的数据
ret = read(0, buf, 1024); //阻塞获取终端输入
for (i = 0; i < 1024; i++)
printf("%c", buf[i]);
return 0;
}
为什么标准输入、标准输出和错误输出都是对应Linux终端?可以在终端看出:
bash表示当前在执行的命令行是shell,它对应的是pts/2这个终端:
再看这三个设备文件的属性:
三者都是软链接文件,再去看各自链接的文件:
可见,最终都是指向/dev/pts/2,所以最终都是在操作终端。
4. 标准库函数和系统调用
标准库函数和系统调用都提供了一些I/O接口(read和write就是I/O操作),同理也提供了打开/关闭文件。以库函数的fopen和系统调用的open为例,它们的原型分别为:
int open(const char *pathname, int flags);
FILE *fopen(const char *path, const char *mode);
fopen的打开文件方式对应open的打开文件方式为:
r O_RDONLY //只读
r+ O_RDWR //可读可写
w O_WRONLY | O_CREAT | O_TRUNC //清空只写
w+ O_RDWR | O_CREAT | O_TRUNC //可读且清空,若不存在则创建
a O_WRONLY | O_CREAT | O_APPEND //可读且追加,若不存在则创建
a+ O_RDWR | O_CREAT | O_APPEND //可读可写且追加,若不存在则创建
c标准库函数是工作在系统调用之上的,也就是说c标准库函数最终都会调用到系统调用函数。例如fopen会调用到open,fopen的返回值是FILE结构体类型指针,FILE结构体原型为:
struct _IO_FILE {
int _flags; /* 相当于open系统调用返回值fd*/
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
可见FILE结构体含有open系统调用返回值fd。
标准库函数在进行对文件读写操作时会先将文件内容读写到(用户态)缓冲区,该缓冲区满足刷新条件时候,再调用系统调用API write函数进行写操作和read函数进行读操作。系统调用再将文件内容写到内核缓冲区中,供内核态函数sys_write、sys_read进行对文件的读写操作。库函数的缓冲区机制提高了系统的读写效率。
下面程序可以验证标准库函数在用户态具有读写缓存机制,而系统调用并没有,它会直接将数据写到内核态缓冲区:
//库函数
int main(void)
{
FILE* fp = NULL;
fp = fopen("./test.txt", "w+");
//往文件里写入数据
fwrite("hello world\n", 1, 20, fp);
//阻塞休眠
sleep(10);
fclose(fp);
return 0;
}
上述程序在阻塞的10s间,在另一终端通过cat命令查看test.txt文件,发现并没有内容,直到进程程退出,确切的说,应该是等待fclose(fp)执行完毕后,那么缓冲区的内容通过系统调用写入到文件了。
//系统调用
int main(void)
{
int fd;
fd = open("./test.txt", O_WRONLY | O_CREAT, 0666);
write(fd, "hello world\n", 20);
//阻塞休眠
sleep(10);
close(fd);
return 0;
}
同样的操作,open函数后休眠10s,在这休眠期间用cat命令查看的文件内容就能够立马打印出来,说明系统调用在用户态并没有缓冲区机制。
注意,这里为什么强调用户态,这是因为Linux底层驱动是有缓冲区机制的。以读写块设备上的文件内容为例,块设备驱动程序具有电梯调度算法,其实现目标也是跟库函数在用户态的缓冲区一样。所以说,系统调用写文件时,用cat命令能够马上读取到文件,其实读取的并非是磁盘上的文件内容,而是内核缓冲区的文件内容。
1) 调用fopen库函数打开test.txt文件
2) 调用fwrite将”hello world\n”字符串写到标准缓冲区
3) 满足刷新缓冲区条件后,调用系统API,即write系统调用进行写操作
4) write将文件内容写到内核缓冲区
5) 如果内核缓冲区没有存满,系统不会立即调用下一层内核函数将内核缓冲区内容写到磁盘。此时若有其他进程来读取test.txt文件的内容,发现内核缓冲区有该文件内容,就直接读取内核缓冲区而不去读取磁盘上的test.txt文件了
另外要注意,每次使用c标准库函数的fopen函数打开一个文件时,会对应一个单独的用户态缓冲区,但是系统调用的内核缓冲区是共用的。
5. 设置库函数的缓冲区
setvbuf()函数是用来设置库函数文件流的缓冲区的,原型为:
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
参数1: stream为文件流指针
参数2: buf位缓冲区的首地址
参数3: type位缓冲区类型取值为
_IOFBF(满缓冲): 当缓冲区为空时,从输入流读取数据放在该缓冲区;当缓冲区为满时,从缓冲区读取数据放到输入流,进而写到文件
_IOLBF(行缓冲): 每次从输入流中读取一行数据或向输入流中写入一行数据,读/写的条件是要满足遇到'\n'
_IONBF(无缓冲): 直接从输入流中读入数据到文件或从文件中直接读取内容放到输出流,中间没有经过缓冲区
参数4: size位缓冲区内字节的数量
对于每个文件,默认的缓冲区大小为8M:
printf("BUFSIZ = %d\n", BUFSIZ);
运行结果:
setvbuf函数使用示例:
int main(void)
{
FILE *fp = NULL;
char buf[1024] = {}; //作为标准库函数I/O缓冲区
fp = fopen("./test.txt", "w+");
//printf("BUFSIZ = %d\n", BUFSIZ);
setvbuf(fp, buf, _IONBF, sizeof(buf)); //无缓冲区
fclose(fp);
return 0;
}
综上,一般在程序设计过程中我们会优先考虑库函数,一方面是考虑到可移植问题,另一方面就是缓冲机制了。缓冲机制对于碎片化写文件操作时,效率有很大提高。据说像上面修改标准库函数缓冲区的操作,就类似于android的中间件开发了。很多时候,考虑到效率等原因,我们并不会直接调用Linux的系统调用,而是使用像库函数这样的东西,但是库函数可能也不能满足特定的需求,那么就需要自己再去修改库函数了或者封装系统调用了。