目录
一、环境信息
二、声明
三、名词解释
1、文件描述符
2、标准输入、标准输出和标准错误
四、实验
1、MyCpNoBuf.c
(1)C源码
(2)函数介绍
2、MyCpBuf.c
(1)C源码
(2)函数介绍
3、MyCpFgetc.c
(1)C源码
(2)函数介绍
4、MyCpFgets.c
(1)C源码
(2)函数介绍
5、makefile
6、编译
7、测试文件
8、对比测试
(1)操作系统cp
(2)MyCpNoBuf
(3)MyCpBuf
(4)MyCpFgetc
(5)MyCpFgets
9、总结
名称 | 值 |
CPU | Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz |
操作系统 | CentOS Linux release 7.9.2009 (Core) |
内存 | 3G |
逻辑核数 | 2 |
本文部分内容参考了《Unix环境高级编程》第三版,这本书写的很好,推荐大家进行阅读。
文件描述符通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都会返回一个文件描述符。在读、写文件时,可以使用这个文件描述符。
每当运行一个新的程序时,所有shell都为其打开3个文件描述符,即标准输入、标准输出、标准错误。如果不做特殊处理,这个三个描述符都会链接向终端。
我们来通过几种C标准函数、系统调用来实现一下复制文件这个功能,来看一下效率上有什么差别。
#include
#include
#include
#include
#define ONE_PAGE_MEM_SIZE 4096
#define EXCEPTION_STATUS -1
int main()
{
int ReadBytes = 0;
char Buf[ONE_PAGE_MEM_SIZE];
while ((ReadBytes = read(STDIN_FILENO,Buf,ONE_PAGE_MEM_SIZE)) > 0)
{
if (ReadBytes != write(STDOUT_FILENO,Buf,ReadBytes))
{
perror("write Error");
exit(EXCEPTION_STATUS);
}
}
if (ReadBytes < 0)
{
perror("read Error");
}
return 1;
}
函数名 | read | write |
头文件 | #include |
#include |
声明 | ssize_t read(int __fd, void *__buf, size_t __nbytes) | ssize_t write(int __fd, const void *__buf, size_t __n) |
参数 | 1、__fd:读取文件的文件句柄。 2、__buf:将读取内容保存的缓冲区。 3、__nbytes:读取内容的字节数。 |
1、__fd:写入文件的文件句柄。 2、__buf:写入内容的缓冲区。 3、__n:写入内容的字节数。 |
返回值 | 返回实际读取的字节数。 | 返回写入的数字,或 -1。 |
描述 | 读取文件。属于系统调用。 1、返回值大于0,读取成功。 2、返回值小于0,读取失败。 3、返回值等于0,没有读取到数据。 |
写入文件。属于系统调用。 1、返回值大于0,写入成功。 2、返回值小于0,写入失败。 3、返回值等于0,没有写入数据。 |
#include
#include
#include
#include
#define EXCEPTION_STATUS -1
int main()
{
int Chr;
while ((Chr = getc(stdin)) != EOF)
{
if (putc(Chr,stdout) == EOF)
{
perror("putc Error");
exit(EXCEPTION_STATUS);
}
}
if (ferror(stdin))
{
perror("getc Error");
}
return 1;
}
函数名 | getc | putc |
头文件 | #include |
#include |
声明 | #define getc(_fp) _IO_getc (_fp) | #define putc(_ch,_fp) _IO_putc (_ch, _fp) |
参数 | 1、_fp:读取文件的文件句柄。 | 1、_ch:写入的字符。 2、_fp:读取文件的文件句柄。 |
返回值 | 成功返回字符,错误或读取到文件末尾返回EOF。 | 成功返回字符,写入失败返回EOF。 |
描述 | 读取一个字符。 | 写一个字符。 |
#include
#include
#include
#include
#define EXCEPTION_STATUS -1
int main()
{
int Chr;
while ((Chr = fgetc(stdin)) != EOF)
{
if (fputc(Chr,stdout) == EOF)
{
perror("fputc Error");
exit(EXCEPTION_STATUS);
}
}
if (ferror(stdin))
{
perror("fgetc Error");
}
return 1;
}
函数名 | fgetc | fputc |
头文件 | #include |
#include |
声明 | int fgetc(FILE *__stream) | int fputc(int __c, FILE *__stream) |
参数 | 1、__stream:读取文件的流。 | 1、__c:写入的字符。 2、__stream:写入文件的流。 |
返回值 | 成功返回字符,错误或读取到文件末尾返回EOF。 | 成功返回字符,写入失败返回EOF。 |
描述 | 从流中读取一个字符。 | 往流中写一个字符。 |
#include
#include
#include
#include
#include
#define EXCEPTION_STATUS -1
#define ONE_PAGE_MEM_SIZE 4096
int main()
{
char ReadStr[ONE_PAGE_MEM_SIZE];
while (fgets(ReadStr,ONE_PAGE_MEM_SIZE,stdin) != NULL)
{
if (fputs(ReadStr,stdout) == EOF)
{
perror("fputs Error");
exit(EXCEPTION_STATUS);
}
}
if (ferror(stdin))
{
perror("fgets Error");
}
return 1;
}
函数名 | fgets | fputs |
头文件 | #include |
#include |
声明 | char *fgets(char *__restrict__ __s, int __n, FILE *__restrict__ __stream) | int fputs(const char *__restrict__ __s, FILE *__restrict__ __stream) |
参数 | 1、__s:读取到字符串。 2、__n:读n-1个字符。 3、__stream:读取文件的流。 |
1、__s:写入的字符串。 2、__stream:写入文件的流。 |
返回值 | 成功返回字符串,错误或读取到文件末尾返回NULL。 | 成功返回最后的字符,写入失败返回EOF。 |
描述 | 从流中读取一个字符串。从流中读n-1个字符,或当遇换行符'\n'、'\0'为止,把读出的内容,存入__s中。与gets不同,fgets在s未尾保留换行符。会自动补充'\0'。 | 往流中写一个字符。 |
CC = gcc
STD_VERSION = -std=gnu11
OPTIMIZATION_LEVEL = -O3 $(STD_VERSION)
CFLAG_O = -c -Wall -Wextra -fpic ${OPTIMIZATION_LEVEL}
CFLAG_SO = -shared -Wall -Wextra ${OPTIMIZATION_LEVEL}
CFLAG_NEW_SO = -shared -Wall -Wextra -fpic ${OPTIMIZATION_LEVEL}
CFLAG_EXEC = -Wall -Wextra ${OPTIMIZATION_LEVEL}
CFLAG_ALIAS = -o
INCLUDE_COMMAND = -I
LIB_COMMAND = -L
LIB_NAME = -l
RM_COMM = rm -rf
UNIX_CODE_PATH = /opt/Developer/ComputerLanguageStudy/C/Unix/
UNIX_SRC_PATH = ${UNIX_CODE_PATH}Src/
UNIX_EXEC_PATH = ${UNIX_CODE_PATH}Exec/
all : MyCpNoBuf
MyCpNoBuf : MyCpBuf
$(CC) $(CFLAG_EXEC) MyCpNoBuf.c $(CFLAG_ALIAS) ${UNIX_EXEC_PATH}MyCpNoBuf
MyCpBuf : MyCpFgetc
$(CC) $(CFLAG_EXEC) MyCpBuf.c $(CFLAG_ALIAS) ${UNIX_EXEC_PATH}MyCpBuf
MyCpFgetc : MyCpFgets
$(CC) $(CFLAG_EXEC) MyCpFgetc.c $(CFLAG_ALIAS) ${UNIX_EXEC_PATH}MyCpFgetc
MyCpFgets :
$(CC) $(CFLAG_EXEC) MyCpFgets.c $(CFLAG_ALIAS) ${UNIX_EXEC_PATH}MyCpFgets
clean :
$(RM_COMM) ${UNIX_EXEC_PATH}*
大家根据自己的目录结构进行调整。
[gbase@czg2 Src]$ ll
总用量 20
-rw-r--r-- 1 root root 1069 10月 27 14:36 makefile
-rw-rw-r-- 1 gbase gbase 406 10月 27 10:46 MyCpBuf.c
-rw-r--r-- 1 root root 410 10月 30 10:18 MyCpFgetc.c
-rw-r--r-- 1 root root 508 10月 30 10:18 MyCpFgets.c
-rw-rw-r-- 1 gbase gbase 538 10月 27 18:10 MyCpNoBuf.c
[gbase@czg2 Src]$ make
gcc -Wall -Wextra -O3 -std=gnu11 MyCpFgets.c -o /opt/Developer/ComputerLanguageStudy/C/Unix/Exec/MyCpFgets
gcc -Wall -Wextra -O3 -std=gnu11 MyCpFgetc.c -o /opt/Developer/ComputerLanguageStudy/C/Unix/Exec/MyCpFgetc
gcc -Wall -Wextra -O3 -std=gnu11 MyCpBuf.c -o /opt/Developer/ComputerLanguageStudy/C/Unix/Exec/MyCpBuf
gcc -Wall -Wextra -O3 -std=gnu11 MyCpNoBuf.c -o /opt/Developer/ComputerLanguageStudy/C/Unix/Exec/MyCpNoBuf
大家可以找个大文本,重复写大一些,测试的效果明显一些,千万不要用dd if=/dev/zero of=../Exec/Data bs=1M count=1000来生成,因为/dev/zero中存的是空字符也就是'0',fgtes方法会读取一个开头字符就停止,达不到效果。我们可以用命令od看一下,-c表示显示以字符方式打印内容。
[root@czg2 gbase]# dd if=/dev/zero bs=1024 count=1 of=Data
记录了1+0 的读入
记录了1+0 的写出
1024字节(1.0 kB)已复制,0.000380848 秒,2.7 MB/秒
[root@czg2 gbase]# od -c Data
0000000 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
0002000
下面这个是我们的测试文件。
[gbase@czg2 Src]$ du -sh /home/gbase/TestData.sql
1.2G /home/gbase/TestData.sql
每一组我们都测试三次,选取最快的结果。
[gbase@czg2 Src]$ time cp /home/gbase/TestData.sql /home/gbase/TestDataBak.sql
real 0m4.791s
user 0m0.005s
sys 0m2.141s
[gbase@czg2 Src]$ du -sh /home/gbase/TestData*
1.2G /home/gbase/TestDataBak.sql
1.2G /home/gbase/TestData.sql
[gbase@czg2 Src]$ time ../Exec/MyCpNoBuf < /home/gbase/TestData.sql > /home/gbase/TestDataBak.sql
real 0m5.056s
user 0m0.026s
sys 0m2.207s
[gbase@czg2 Src]$ du -sh /home/gbase/TestData*
1.2G /home/gbase/TestDataBak.sql
1.2G /home/gbase/TestData.sql
[gbase@czg2 Src]$ time ../Exec/MyCpBuf < /home/gbase/TestData.sql > /home/gbase/TestDataBak.sql
real 0m16.911s
user 0m13.576s
sys 0m3.266s
[gbase@czg2 Src]$ du -sh /home/gbase/TestData*
1.2G /home/gbase/TestDataBak.sql
1.2G /home/gbase/TestData.sql
[gbase@czg2 Src]$ time ../Exec/MyCpFgetc < /home/gbase/TestData.sql > /home/gbase/TestDataBak.sql
real 0m14.686s
user 0m12.103s
sys 0m2.549s
[gbase@czg2 Src]$ du -sh /home/gbase/TestData*
1.2G /home/gbase/TestDataBak.sql
1.2G /home/gbase/TestData.sql
[gbase@czg2 Src]$ time ../Exec/MyCpFgets < /home/gbase/TestData.sql > /home/gbase/TestDataBak.sql
real 0m4.779s
user 0m1.197s
sys 0m2.147s
[gbase@czg2 Src]$ du -sh /home/gbase/TestData*
1.2G /home/gbase/TestDataBak.sql
1.2G /home/gbase/TestData.sql
从测试结果来看read、write、fgets、fputs效率上较高,但read、write属于系统调用,并不适用于平台移植,fgets、fputs属于C标准库,方便移植推荐。