Linux下C语言实现文件读写操作(包含在应用层与驱动层)

前言:

第1节是笔者基于公司的“祖传”C语言操作模板上做相应修改的程序,当做自己以后使用C语言文件操作的模板。
第2节与第3节是笔者之前需要配合团队内的小伙伴做数据转换,他需要我将保存在.txt文件内的字符数据(十六进制)转换为二进制,也需要我将.Bin文件内的二进制数据转换为字符数据(十六进制)。当然这两节程序有很大的优化空间,弊端为每次读取数据的量太小,需要频繁IO操作,导致运行速度变得很慢。解决方法为使用malloc函数一次性将文件的内容读取到内存空间上,可大幅提高程序的运行速度。
第4节是笔者一直很好奇32位操作系统下的C语言的数据类型所占空间进行测试的例子,方便自己在后面的开发中准确的使用数据类型,不至于让数据小而空间大,或者数据大而空间小。
第5节是笔者想通过C语言为工程项目中修改配置文件中的参数,通过匹配配置文件中指定的变量去替换参数。但该节程序最后并未用于工程项目中,笔者想表达,C语言实现真的好麻烦,这就是更高级的编程语言强大之处。
第6节是笔者想为嵌入式Linux产品的开机Logo动态读取指定路径下的.ppm图片,原本考虑修改Kernel中显示Logo部分,将图片的数据写入相应的地方。但后面在内核源码中找不到内核编译时怎么将logo_linux_clut224.ppm编译为logo_linux_clut224.c的方法,不清楚.ppm文件是怎么将数据转化到struct linux_logo结构体中的clut和data成员,后面不得不放弃这种想法。但也让笔者改变思路,让驱动程序读取指定路径下的配置文件,让驱动定向运行。
第7节为应用层的常规文件操作。
第8节为获取挂载在文件系统下节点的存储情况。
第9节摘抄自《Linux程序设计 第4版》中第3、4节的C语言进行文件/目录、系统操作
第10节为记录程序运行的时间

下面的程序参考了网上的许多例子,测试可用,若需要使用,需要修改部分参数。

1. C语言文件操作的模板:

// 编译与运行命令:
// gcc xxxx.c -o xxxx
// ./xxxx

#include <stdio.h>
#include <string.h>

int OpenFile(char *ReadFile, char *WriteFile, unsigned int *FileLength,
					FILE **fpRead, FILE **fpWrite);
void CloseFile(FILE *fpRead, FILE *fpWrite);
int FileOperation(unsigned int FileLength, FILE *fpRead, FILE *fpWrite);

int main(int argc, char *argv[])
{
	char ReadFile[] = "ReadFile.txt";
	char WriteFile[] = "WriteFile.txt";
	
	FILE *fpRead = NULL, *fpWrite = NULL;
	unsigned int FileLength = 0;
	
	int ret = 0;
	
	// 打开需要读、写的文件
	ret = OpenFile(ReadFile, WriteFile, &FileLength, &fpRead, &fpWrite);
	if(ret < 0)
	{
		return 0;
	}
	
	// 文件操作
	FileOperation(FileLength, fpRead, fpWrite);
	
	// 关闭文件
	CloseFile(fpRead, fpWrite);
	
	return 0;
}

int OpenFile(char *ReadFile, char *WriteFile, unsigned int *FileLength,
					FILE **fpRead, FILE **fpWrite)
{
	// 打开需要读的文件
	*fpRead = fopen(ReadFile, "r+");
	if(*fpRead == NULL)
	{
		printf("open %s error \n", ReadFile);
		return -1;
	}
	
	// 打开需要写的文件
	*fpWrite = fopen(WriteFile, "wb");
	if(*fpWrite == NULL)
	{
		printf("open %s error \n", WriteFile);
		fclose(*fpRead);
		return -1;
	}
	
	// 将读文件指针定位于文件的末尾处,获取文件大小
	fseek(*fpRead, 0, SEEK_END);
	*FileLength = ftell(*fpRead);
	printf("FileLength = %dB (%.2fMB) \n", *FileLength,
					((double)*FileLength / (1024 * 1024)));
	
	return 1;
}

void CloseFile(FILE *fpRead, FILE *fpWrite)
{
	fclose(fpRead);
	fclose(fpWrite);
}

int FileOperation(unsigned int FileLength, FILE *fpRead, FILE *fpWrite)
{
	unsigned int i = 0;
	unsigned char Array[3];
	unsigned char CharLen = 2;
	
	// 将读、写文件指针指向文件头
	fseek(fpRead, 0, SEEK_SET);
	fseek(fpWrite, 0, SEEK_SET);
	
	// 进行文件读写
	for(i = 0; i < FileLength; i += 2)
	{
		if((i != 0) && (i % 16 == 0))
		{
			fwrite("\n", sizeof(unsigned char), 1, fpWrite);
		}
		
		memset(Array, '\0', sizeof(Array));
		
		fread(Array, sizeof(unsigned char), CharLen, fpRead);
		
		fwrite(Array, sizeof(unsigned char), CharLen, fpWrite);
		fwrite(" ", sizeof(unsigned char), 1, fpWrite);
	}
	
	return 1;
}
FILE结构体:
// 位于stdio.h,笔者在Linux底下找不到定义FILE结构体,只在Visual Studio内找到了定义
// 不同编译器下的FILE结构体定义不一样
// FILE结构是间接地操作系统的文件控制块 (FCB)来实现对文件的操作

#ifndef _FILE_DEFINED
struct _iobuf {
        char *_ptr;       // 文件输入的下一个位置 
        int   _cnt;       // 当前缓冲区的相对位置 
        char *_base;      // 指基础位置(即是文件的其始位置)
        int   _flag;      // 文件标志
        int   _file;      // 应该是文件描述符,进入打开文件列表索引的整数
        int   _charbuf;
        int   _bufsiz;
        char *_tmpfname;
        };
typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif

2. 将字符(十六进制)转成二进制(C语言的文件操作):

// 编译与运行命令:
// gcc xxxx.c -o xxxx -lm
// ./xxxx

#include <stdio.h>
#include <string.h>
#include <stdlib.h> 
#include <errno.h>
#include <math.h>
#include <unistd.h> 
#include <sys/time.h>

int main(int argc, char *argv[])
{
	char FilePath[] = "/mnt/hgfs/XXXX/";
	char ReadFile[] = "File";
	char WriteFile[] = "FileConvert";
	char TxtUnit[] = ".txt";
	char BinUnit[] = ".bin";
	
	char ReadFilePath[80], WriteFilePath[80];
	char ClearWriteFileCmd[160], TxtToJPEGCmd[160];
	
	unsigned int FileLength = 0;
	
	char array[4];
	unsigned char DataBin = 0;
	unsigned char DataTemp = 0;
	
	int i = 0, j = 0;
	FILE *fpRead = NULL, *fpWrite = NULL;
	unsigned int NumRead = 0, NumWrite = 0;
	unsigned int DataCount = 0;
	int CharLen = 2;		// 每次读取文件的两个字符
	
	memset(ReadFilePath, 0, 80 * sizeof(char));
	memset(WriteFilePath, 0, 80 * sizeof(char));
	memset(ClearWriteFileCmd, 0, 160 * sizeof(char));
	memset(TxtToJPEGCmd, 0, 160 * sizeof(char));
	
	sprintf(ReadFilePath, "%s%s%s", FilePath, ReadFile, TxtUnit);
	sprintf(WriteFilePath, "%s%s%s", FilePath, WriteFile, TxtUnit);
	sprintf(ClearWriteFileCmd, "dd if=/dev/null of=%s%s%s", FilePath, WriteFile, TxtUnit);
	sprintf(TxtToJPEGCmd, "mv %s%s%s %s%s%s", FilePath, WriteFile, TxtUnit, FilePath, WriteFile, BinUnit);
	
	// 打开需要读的文件
	fpRead = fopen(ReadFilePath , "r+");
	if(fpRead == NULL)
	{
		printf("Open %s error \n", ReadFilePath);
		return -1 ;
	}
	// 打开需要写的文件
	fpWrite = fopen(WriteFilePath , "wb");
	if(fpWrite == NULL)
	{
		printf("Open %s error \n", WriteFilePath);
		return -1 ;
	}
	
	printf("Open %s Success \n", ReadFilePath);
	printf("Open %s Success \n", WriteFilePath);
	
	system(ClearWriteFileCmd);
	
	// 定位于文件的末尾处,获取文件大小
	fseek(fpRead, 0, SEEK_END);
	FileLength = ftell(fpRead);
	printf("FileLength = %d (%.2fK %.2fM) \n", FileLength, ((double)FileLength / 1024), ((double)FileLength / 1024 / 1024));
	
	// 将文件指针指向文件头
	fseek(fpRead, 0, SEEK_SET);
	fseek(fpWrite, 0, SEEK_SET);
	
	NumRead = 0;
	NumWrite = 0;
	
	while(NumRead < FileLength)
	{
		memset(&array, '\0', 4 * sizeof(char));
		
		fseek(fpRead, NumRead, SEEK_SET);
		fread(array, sizeof(char), CharLen, fpRead);
		
		// 判断取出的值是否为换行符、空格、制表符、回车符
		//if((array[0] == '\n') || (array[0] == ' ') || (array[0] == '\t') || (array[0] == '\r'))
		if(array[0] <= ' ')
		{
			NumRead += (1 * sizeof(char));
			continue ;
		}
		
		NumRead += (CharLen * sizeof(char));
		
		// 下面这句话不能写,会大大拖慢速度,运行速度降低150倍左右
		//fseek(fpWrite, NumWrite, SEEK_SET);
		
		// 字符转为数据
		for(DataBin = 0 , j = 0 ; j < CharLen ; j ++)
		{
			DataTemp = array[j] ;
			if((DataTemp >= '0') && (DataTemp <= '9'))		DataTemp = DataTemp - '0';
			else if((DataTemp >= 'a') && (DataTemp <= 'z'))	DataTemp = (DataTemp - 'a' + 10);
			else if((DataTemp >= 'A') && (DataTemp <= 'Z'))	DataTemp = (DataTemp - 'A' + 10);
			
			//DataBin += (DataTemp * pow(16, (CharLen - 1 - j)));
			DataBin += (DataTemp << (4 * (CharLen - 1 - j)));
		}
		fwrite(&DataBin, sizeof(char), 1, fpWrite);
		NumWrite += (1 * sizeof(char));
		DataCount ++ ;
		
	}
	
	fclose(fpRead);
	fclose(fpWrite);
	
	system(TxtToJPEGCmd);
	
	return 0 ;
}

3. 将二进制转成字符(十六进制)(C语言的文件操作):

// 编译与运行命令:
// gcc xxxx.c -o xxxx -lm
// ./xxxx

#include <stdio.h>
#include <string.h>
#include <stdlib.h> 
#include <errno.h>
#include <math.h>
#include <unistd.h> 
#include <sys/time.h>

int FileConvert(char *FilePath, char *ReadFile, char *WriteFile);
int main(int argc, char *argv[])
{
	char FilePath[] = "/mnt/hgfs/Share_Ubuntu/FileDispose/Bin01/";
	char FileName[] = "rgb";
	char ReadFile[50], WriteFile[50];
	unsigned int FileCounter = 1, FileNum = 100;
	
	int ret = 0;
	
	while(FileCounter <= FileNum)
	{
		memset(ReadFile, 0, sizeof(ReadFile));
		memset(WriteFile, 0, sizeof(WriteFile));
		sprintf(ReadFile, "%s%02d", FileName, FileCounter);
		sprintf(WriteFile, "%sConvert", ReadFile);
		FileCounter++;
		
		ret = FileConvert(FilePath, ReadFile, WriteFile);
		if(ret < 0)
		{
			printf("Read and Write %s error \n", ReadFile);
			continue;
		}
		
	}
	
	return 0 ;
}

int FileConvert(char *FilePath, char *ReadFile, char *WriteFile)
{
	FILE *fpRead = NULL, *fpWrite = NULL;
	int CharLen = 1;		// 每次读取文件的一个字符
	unsigned char array[4];
	unsigned char DataBin = 0;
	unsigned char DataTemp = 0;
	int i = 0, j = 0;
	unsigned int DataCount = 0;
	
	char TxtUnit[] = ".txt", JpgUnit[] = ".jpg", BinUnit[] = ".bin";
	char ReadFilePath[80], WriteFilePath[80];
	char ClearWriteFileCmd[160];
	
	memset(ReadFilePath, 0, sizeof(ReadFilePath));
	memset(WriteFilePath, 0, sizeof(WriteFilePath));
	memset(ClearWriteFileCmd, 0, 160 * sizeof(char));
	
	sprintf(ReadFilePath, "%s%s%s", FilePath, ReadFile, BinUnit);
	sprintf(WriteFilePath, "%s%s%s", FilePath, WriteFile, TxtUnit);
	sprintf(ClearWriteFileCmd, "dd if=/dev/null of=%s > /dev/null 2>&1", WriteFilePath);
	
	
	// 打开需要读的文件
	fpRead = fopen(ReadFilePath , "r+");
	if(fpRead == NULL)
	{
		printf("Open %s error \n", ReadFilePath);
		return -1 ;
	}
	// 打开需要写的文件
	fpWrite = fopen(WriteFilePath , "wb");
	if(fpWrite == NULL)
	{
		printf("Open %s error \n", WriteFilePath);
		return -1 ;
	}
	
	system(ClearWriteFileCmd);
	
	printf("Open %s Success \n", ReadFilePath);
	printf("Open %s Success \n", WriteFilePath);
	
	unsigned int FileLength = 0;
	
	// 定位于文件的末尾处,获取文件大小
	fseek(fpRead, 0, SEEK_END);
	FileLength = ftell(fpRead);
	printf("(%s) FileLength = %d B (%.2f KB, %.2f MB) \n", ReadFile,
							FileLength,
							((double)(FileLength >> 10)),
							((double)(FileLength >> 20)));
	
	// 将文件指针指向文件头
	fseek(fpRead, 0, SEEK_SET);
	fseek(fpWrite, 0, SEEK_SET);
	
	unsigned int NumRead = 0, NumWrite = 0;
	
	while(NumRead < FileLength)
	{
		if((NumRead != 0) && (NumRead % 3 == 0))
		{
			fwrite(" ", sizeof(char), 1, fpWrite);
		}
		
		memset(&DataBin, '\0', sizeof(DataBin));
		memset(array, '\0', sizeof(array));
		
		fread(&DataBin, sizeof(unsigned char), CharLen, fpRead);
		NumRead += (CharLen * sizeof(unsigned char));
		
		array[0] = (DataBin & 0xF0) >> 4;		// 提取十位
		array[1] = (DataBin & 0x0F) >> 0;		// 提取个位
		
//		printf("DataBin = 0x%X, array[0] = 0x%X, array[1] = 0x%X \n", DataBin, array[0], array[1]);
		
		// 二进制转换为字符的计算
		for(j = 0 ; j < 2 ; j ++)
		{
			if((array[j] >= 0x0) && (array[j] <= 0x9))		array[j] = array[j] + '0';
			else if((array[j] >= 0xA) && (array[j] <= 0xF))	array[j] = array[j] - 0xA + 'A';
		}
		
		fwrite(&array, sizeof(char), 2, fpWrite);
		
	}
	
	fclose(fpRead);
	fclose(fpWrite);
	
}

4. 验证char系列、int系列、long系列、float系列、struct类型的存储空间,数据大小端的存储空间

当前目录的文件结构:
tree test/
test/
├── 1.h
├── a.c
├── b.c
├── c.c
└── Makefile

4.1 1.h文件内容:
#ifndef __1_H
#define __1_H

#include <stdio.h>
#include <float.h>
#include <limits.h>

struct sData{
	unsigned int uint32_data_1;
	unsigned long long uint64_data_1;
	unsigned char uint8_data_1;
	unsigned char uint8_data_2;
	unsigned int uint32_data_2;
	unsigned short int uint16_data_1;
	unsigned char uint8_data_3;
	unsigned int uint32_data_3;
};

int b_fun(void);
int c_fun(void);

#endif
4.2 a.c文件内容:
#include <stdio.h>
#include "1.h"

int main(int argc , char* argv[])
{
	b_fun();
	
	c_fun();
	
	return 0;
}

4.3 b.c文件内容:
#include "1.h"
#include "string.h"

int b_fun(void)
{
	printf("\n== b_fun == \n");
	printf("\n============ char ============\n");
	printf("sizeof(char) = %d \n", sizeof(char));						// 1
	printf("sizeof(unsigned char) = %d \n", sizeof(unsigned char));		// 1
	printf("sizeof(signed char) = %d \n", sizeof(signed char));			// 1
	printf("\n============ short int ============\n");
	printf("sizeof(short int) = %d \n", sizeof(short int));						// 2
	printf("sizeof(unsigned short int) = %d \n", sizeof(unsigned short int));	// 2
	printf("sizeof(signed short int) = %d \n", sizeof(signed short int));		// 2
	printf("\n============ int ============\n");
	printf("sizeof(int) = %d \n", sizeof(int));						// 4
	printf("sizeof(unsigned int) = %d \n", sizeof(unsigned int));	// 4
	printf("sizeof(signed int) = %d \n", sizeof(signed int));		// 4
	printf("\n============ long int ============\n");
	printf("sizeof(long int ) = %d \n", sizeof(long int));						// 4
	printf("sizeof(unsigned long int) = %d \n", sizeof(unsigned long int));		// 4
	printf("sizeof(signed long int) = %d \n", sizeof(signed long int));			// 4
	printf("\n============ long long ============\n");
	printf("sizeof(long long) = %d \n", sizeof(long long));						// 8
	printf("sizeof(signed long long) = %d \n", sizeof(signed long long));		// 8
	printf("sizeof(unsigned long long) = %d \n", sizeof(unsigned long long));	// 8
	printf("\n============ float ============\n");
	printf("sizeof(float) = %d \n", sizeof(float));						// 4
	printf("\n============ double ============\n");
	printf("sizeof(double) = %d \n", sizeof(double));					// 8
	printf("\n\n");
	
	unsigned char uint8_Array[10];
	unsigned short int uint16_Array[10];
	unsigned int uint32_Array[10];
	unsigned long long uint64_Array[10];
	float float32_Array[10];
	double float64_Array[10];
	
	memset(uint8_Array, '\0', 10 * sizeof(unsigned char));
	memset(uint16_Array, '\0', 10 * sizeof(unsigned short int));
	memset(uint32_Array, '\0', 10 * sizeof(unsigned int));
	memset(uint64_Array, '\0', 10 * sizeof(unsigned long long));
	memset(float32_Array, '\0', 10 * sizeof(float));
	memset(float64_Array, '\0', 10 * sizeof(double));
	printf("sizeof(uint8_Array) = %d \n", sizeof(uint8_Array));
	printf("sizeof(uint16_Array) = %d \n", sizeof(uint16_Array));
	printf("sizeof(uint32_Array) = %d \n", sizeof(uint32_Array));
	printf("sizeof(uint64_Array) = %d \n", sizeof(uint64_Array));
	printf("sizeof(float32_Array) = %d \n", sizeof(float32_Array));
	printf("sizeof(float64_Array) = %d \n", sizeof(float64_Array));
	printf("\n\n");
	
	uint8_Array[0] = '1';
	printf("strlen(uint8_Array) = %d \n", strlen(uint8_Array));
	printf("\n\n");
	struct sData data1;
	printf("==~~ sizeof(data1) = %d, add sizeof( ... + ...) = %d ~~== \n", sizeof(data1),
			sizeof(data1.uint32_data_1) + sizeof(data1.uint8_data_1) +
			sizeof(data1.uint8_data_2) + sizeof(data1.uint32_data_2) +
			sizeof(data1.uint16_data_1) + sizeof(data1.uint8_data_3) +
			sizeof(data1.uint32_data_3) + sizeof(data1.uint64_data_1)
	);
	printf("\n\n");
	
	return 1;
}
4.4 c.c文件内容:
#include "1.h"

int c_fun(void)
{
	printf("\n== c_fun == \n");
	printf("\n============ char min max dig ============\n");
	printf("CHAR_MIN( char MinValue ) = 0x%X \n", CHAR_MIN);
	printf("CHAR_MAX( char MaxValue ) = 0x%X \n", CHAR_MAX);
	printf("\n============ int min max dig ============\n");
	printf("INT_MIN( int MinValue ) = 0x%X \n", INT_MIN);
	printf("INT_MAX( int MaxValue ) = 0x%X \n", INT_MAX);
	printf("\n============ float min max dig ============\n");	// %E
	printf("FLT_MIN( float MinValue ) = %f \n", FLT_MIN);
	printf("FLT_MAX( float MaxValue ) = %f ( %E )\n", FLT_MAX, FLT_MAX);
	printf("FLT_DIG( float PrecisionValue ) = %d \n", FLT_DIG);
	printf("\n============ double min max dig ============\n");
	printf("DBL_MIN( double MinValue ) = %f \n", DBL_MIN);
	printf("DBL_MAX( double MaxValue ) = %f ( %E ) \n", DBL_MAX, DBL_MAX);
	printf("DBL_DIG( double PrecisionValue ) = %d \n", DBL_DIG);
	
	unsigned long long FloatConvert = 0;
	unsigned long long DoubleConvert = 0;
	FloatConvert = (unsigned long long)FLT_MAX;
	DoubleConvert = (unsigned long long)DBL_MAX;
	printf("FloatConvert = 0x%llX, DoubleConvert = 0x%llX \n\n", FloatConvert, DoubleConvert);
	
	unsigned int x = 0x87654321;		// 高位为8,低位为1
	unsigned char *p = (unsigned char *)&x;
	int i = 0;
	
	// 数据最左为高位,最右为低位
	// 小端模式:低地址低位数据,高地址高位数据
	// 大端模式:低地址高位数据,高地址低位数据
	printf("x = 0x%X  \n", x);
	for(i = 0; i < 4; i++)
	{
		printf("0x%X(Addr) = 0x%X \n", (unsigned int)(p + i), p[i]);
	}
	
	if(*p == 0x21)
	{
		printf("Little endian mode!\n\n");
	}
	else
	{
		printf("Big endian mode!\n\n");
	}
	
	return 1;
}
4.5 Makefile文件内容:
# 目标文件的运行平台: ARM x86 MIPS RISC
#RUN_ARCH := x86
RUN_ARCH := ARM

# 目标文件名,输入任意你想要的执行文件名
TARGET_FILE  := TestSizeof_MF
TARGET_PATH  := ~/nfs/

# 编译工具
ifeq ($(RUN_ARCH), ARM)
	CROSS := /opt/gcc-linaro-arm-linux-gnueabihf-4.7-2012.11-20121123_linux/bin/arm-linux-gnueabihf-
else ifeq ($(RUN_ARCH), x86)
	CROSS := 
endif
# CROSS = arm-linux-gnueabihf-
export CC       = $(CROSS)gcc
export CXX      = $(CROSS)g++
export AR       := $(CROSS)ar
export AS       := $(CROSS)as
export STRIP    := $(CROSS)strip
export CPP      = $(CC) -E
export OBJCOPY  = $(CROSS)objcopy
export OBJDUMP  = $(CROSS)objdump
export NM       = $(CROSS)nm
export LD       = $(CROSS)ld

# 源文件,自动找所有.c和.cpp文件,并将目标定义为同名.o文件
SOURCE  := $(wildcard *.c) $(wildcard *.cpp)
OBJS    := $(patsubst %.c,%.o,$(patsubst %.cpp,%.o,$(SOURCE)))

# 编译参数
LIBS    := #-L /Path -lname
LDFLAGS :=
DEFINES :=
INCLUDE := #-I /Path/include/
CFLAGS  := -g -Wall -O3 $(DEFINES) $(INCLUDE)
CXXFLAGS:= $(CFLAGS) -DHAVE_CONFIG_H


# 下面的基本上不需要做任何改动了
.PHONY : everything objs clean veryclean rebuild
everything : $(TARGET_FILE)
all : $(TARGET_FILE)
objs : $(OBJS)
rebuild: veryclean everything
clean :
	rm -rf *.o $(TARGET_FILE)
veryclean : clean
	rm -rf $(TARGET_FILE)

$(TARGET_FILE) : $(OBJS)
	$(CC) $(CXXFLAGS) -o $@ $(OBJS) $(LDFLAGS) $(LIBS)
# 应用程序需要删掉的编译中间文件
	rm -rf *.o
# 拷贝目标文件
	cp $(TARGET_FILE) $(TARGET_PATH)

5. 文件内容替换/修改(旧字符串与新字符串大小不等长):

#if 0
程序思路:
1. 先比较新、旧字符串的长度,判断偏移位置的长度与方向
2. 查找目标行,假如找到,则读取目标行往后至文本结束的所有数据到Buffer
3. 替换目标行的数据
4. 将Buffer的数据导入替换完目标行的数据后面
5. 假如旧字符串数据比新字符串数据长,则截断多余文本长度

笔者注:
该程序主要用于工程项目中修改配置文件中的参数,通过匹配配置文件中指定的变量去替换参数。
程序亲测可用,替换的本质是对指定行的字符串替换,字符串的长度不限制,可过长也可过短,
也可不变,因此有一定的局限性。
程序优点:运行过程中不创建新的文本,减少对NandFlash的擦除与进入。
后面过来学习的同学可以对程序进行修改,直到功能满足自己的需求
#endif
# 原文本(程序运行前的)内容
$ cat ChangeFile.txt
[This is a Configuration File]
Parameter_A=0123456789
Parameter_B=0123456789
Parameter_C=0123456789
Parameter_D=0123456789
Parameter_E=0123456789
Parameter_F=0123456789
Parameter_G=0123456789

# 程序运行后的内容
$ cat ChangeFile.txt
[This is a Configuration File]
Parameter_A=0123456789
Parameter_B=0123456789
Parameter_C=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
Parameter_D=0123456789
Parameter_E=0123456789
Parameter_F=0123456789
Parameter_G=0123456789
// 程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

// 用于消除警告fgets、fread、ftruncate等函数的编译警告
// 适合喜欢“0 errors, 0 warnings”的同学
char *pEliminateWarn = NULL;
int EliminateWarn = 0;

int OpenFile(char *ChangeFile, unsigned int *FileLength, FILE **fp);
int FileOperation(unsigned int FileLength, FILE *fp, char *OldStr,
					char *NewStr, int StrDiffer);
void CloseFile(FILE *fp);
void SetFileLength(char *ChangeFile, unsigned int FileLength, int StrDiffer);

int main(int argc, char *argv[])
{
	char ChangeFile[] = "ChangeFile.txt";
	char OldStr[] = "Parameter_C=0123456789";
	char NewStr[] = "Parameter_C=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
	int StrDiffer = 0;			// OldStr 与 NewStr 之差

	FILE *fp = NULL;
	unsigned int FileLength = 0;
	int ret = 0;
	
	// StrDiffer > 0 说明文本后面的数据需要往前移动,且旧文本比新文本 长,
	//               文本末尾需要 截断 StrDiffer 个字符长度;
	// StrDiffer < 0 说明文本后面的数据需要往后移动,且旧文本比新文本 短
	//               文本末尾需要 增长 StrDiffer 个字符长度;
	// StrDiffer = 0 说明文本后面的数据不需要移动,且旧文本与新文本 等长
	//               文本末尾 不变。
	StrDiffer = strlen(OldStr) - strlen(NewStr);
	
	// 1.打开需要修改的文件
	ret = OpenFile(ChangeFile, &FileLength, &fp);
	if(ret < 0)
	{
		return 0;
	}
	
	// 2.文件操作
	FileOperation(FileLength, fp, OldStr, NewStr, StrDiffer);
	
	// 3.关闭文件
	CloseFile(fp);
	
	// 4.如果(OldStr > NewStr),需截断文本末尾的空余,即设置文本长度
	if(StrDiffer > 0)
	{
		SetFileLength(ChangeFile, FileLength, StrDiffer);
	}
	
	return 0;
}

int OpenFile(char *ChangeFile, unsigned int *FileLength, FILE **fp)
{
	// 打开需要修改的文件
	*fp = fopen(ChangeFile, "rwb+");
	if(*fp == NULL)
	{
		printf("open %s error \n", ChangeFile);
		return -1;
	}
	
	// 将文件指针定位于文件的末尾处,获取文件大小
	fseek(*fp, 0, SEEK_END);
	*FileLength = ftell(*fp);
	printf("FileLength = %dB (%.2fMB) \n", *FileLength,
					((double)*FileLength / (1024 * 1024)));
	
	return 1;
}

void CloseFile(FILE *fp)
{
	fclose(fp);
}

void SetFileLength(char *ChangeFile, unsigned int FileLength, int StrDiffer)
{
	int fd = 0;
	unsigned int length = 0;
	
	fd = open(ChangeFile, O_RDWR);
	
	length = FileLength - StrDiffer;
	EliminateWarn = ftruncate(fd, length);
	
	printf("==== length = %d === \n", length);
	
	close(fd);
}

int FileOperation(unsigned int FileLength, FILE *fp, char *OldStr,
					char *NewStr, int StrDiffer)

{
	// 将文件指针指向文件头
	fseek(fp, 0, SEEK_SET);
	
	// 进行相应的文件操作
	unsigned int RemainFileLength = 0;		// 剩余的长度
	unsigned int CurrentFileLength = 0; 	// 当前的长度
	
	unsigned char *RemainFileData = NULL;	// 剩下的文本数据
	
	char Array[500];		// 存储文本的行数据
	int ArrayLength = 0;	// 记录该行数据的长度
	
	while(1)
	{
		// 1.提取文本的行数据
		memset(Array, '\0', sizeof(Array));
		if(fgets(Array, sizeof(Array), fp) == NULL)
		{
			break;
		}
		
		// 2.记录文本的行数据长度
		ArrayLength = strlen(Array);
		
		// 3.比较该行是否为目标行
		if(strncmp(Array, OldStr, ArrayLength - 1) == 0)
		{
			// 4.定位文本中在OldStr后第1位(即“\n”)的位置
			CurrentFileLength = ftell(fp);
			
			// 5.读取出文本中在OldStr后面不需要修改的文本数据 到 RemainFileData
			RemainFileLength = FileLength - CurrentFileLength;
			RemainFileData = (unsigned char *)malloc(RemainFileLength + 1);
			memset(RemainFileData, 0, (RemainFileLength + 1));
			EliminateWarn = fread(RemainFileData, sizeof(unsigned char), (RemainFileLength), fp);
			
			// 6.将文件指针指向OldStr前第1位(即“\n”)的位置,将NewStr替换OldStr
			memset(Array, '\0', sizeof(Array));
			strcpy(Array, NewStr);
			fseek(fp, (CurrentFileLength - ArrayLength), SEEK_SET);
			fprintf(fp, "%s", Array);
			
			// 7.在文本中NewStr后第1位补上换行符(“\n”)
			fwrite("\n", sizeof(unsigned char), 1, fp);
			
			// 8.将RemainFileData中的数据导入到文本NewStr后,并释放RemainFileData
			fwrite(RemainFileData, sizeof(unsigned char), RemainFileLength, fp);
			free(RemainFileData);
			
			// 9.重新将文件指针定位到一开始的位置
			fseek(fp, CurrentFileLength, SEEK_SET);
			
		}
		
	}
	
	return 1;
}

6. 驱动中的文件操作:

6.1 程序部分:
// 笔者对内核态下的文件操作只停留在驱动程序读取文本内容,根据读取配置文件的信息,
// 驱动加载时进行定向运行,但深入的应用并不了解

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/kernel.h>
#include <linux/slab.h>

int __init driver_file_init(void)
{
	mm_segment_t fs;
	
	struct file *fpRead = NULL, *fpWrite = NULL;
	struct kstat *StatRead = NULL;
	char *pFileBuf = NULL;
	
	loff_t PosRead = 0, PosWrite = 0;
	unsigned long long FileLength = 0;
	
	const char ReadFile[] = "/tmp/ReadFile.txt";
	const char WriteFile[] = "/tmp/WriteFile.txt";
	
	printk("driver_file enter \n");
	
	// 1.获取内核态默认的内存地址限制,设置内存地址限制覆盖到用户空间
	fs = get_fs();
	set_fs(KERNEL_DS);
	
	// 2.获取相应文件的大小
	StatRead = (struct kstat *)kmalloc(sizeof(struct kstat), GFP_KERNEL);
	memset(StatRead, 0, sizeof(struct kstat));
	vfs_stat(ReadFile, StatRead);
	FileLength = StatRead->size;
	kfree(StatRead);
	
	// 3.打开读、写文件
	fpRead = filp_open(ReadFile, O_RDWR | O_CREAT, 0644);
	if(IS_ERR(fpRead))
	{
		printk("Open %s read file error \n", ReadFile);
		goto open_read_file_fail;
	}
	
	fpWrite = filp_open(WriteFile, O_RDWR | O_CREAT, 0644);
	if(IS_ERR(fpWrite))
	{
		printk("Open %s write file error \n", WriteFile);
		goto open_write_file_fail;
	}
	
	// 4.清空Buffer
	pFileBuf = (char *)kmalloc(FileLength * sizeof(char) + 1, GFP_KERNEL);
	memset(pFileBuf, 0, FileLength * sizeof(char) + 1);
	
	// 5.将读文件的内容导入到Buffer,再把Buffer的内容导入到写文件
	PosWrite = PosRead = 0;
	vfs_read(fpRead, pFileBuf, (FileLength * sizeof(char)), &PosRead);
	vfs_write(fpWrite, pFileBuf, (FileLength * sizeof(char)), &PosWrite);
	
	printk(KERN_INFO "==== pFileBuf = %s ==== \n", pFileBuf);
	
	// 6.释放Buffer
	kfree(pFileBuf);
	
	// 7.关闭读写文件
	filp_close(fpRead, NULL);
	filp_close(fpWrite, NULL);
	
	// 8.设置内核态的内存地址限制为默认的内存地址限制
	set_fs(fs);
	
	return 0;
	
open_write_file_fail:
	filp_close(fpRead, NULL);
open_read_file_fail:
	set_fs(fs);
	return -1;
}

void __exit driver_file_exit(void)
{
	printk("driver_file exit \n");
}

module_init(driver_file_init);
module_exit(driver_file_exit);
MODULE_LICENSE("GPL");
6.2 对struct kstat结构体的解析:
struct kstat结构体位于 include/linux/stat.h,Linux 3.10.31-LTSI

struct kstat {
	u64				ino;		// inode number,inode节点号
	dev_t			dev;		// ID of device containing file,文件所在设备的ID
	umode_t			mode;		// protection,保护模式
	unsigned int	nlink;		// number of hard links,链向此文件的连接数(硬连接)
	kuid_t			uid;		// user ID of owner,user id
	kgid_t			gid;		// group ID of owner,group id
	dev_t			rdev;		// device ID (if special file),设备号,针对设备文件
	loff_t			size;		// total size, in bytes,文件大小,字节为单位
	struct timespec		atime;		// time of last access,最近访问时间
	struct timespec		mtime;		// time of last modification,最近修改时间
	struct timespec		ctime;		// time of last status change,创建时间
	unsigned long		blksize;	// blocksize for filesystem I/O,系统块的大小
	unsigned long long	blocks;		// number of blocks allocated,文件所占块数
};

7. 常规操作中的文件操作:

int Temp;

// 将文件指针定位到文件头
fseek(fp, 0, SEEK_SET);

while(1)
{
	// 从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动。
	Temp = fgetc(fp);
	
	// 该判断只适合fgetc
	if(Temp == EOF)
	{
		// 把文件流里的所有未写出数据立刻写出
		fflush(fp);
		printf("\n");
		
		printf("File point have reached the end of file Or Read error \n");
		sleep(5);
	}
	
	// 该判断具有通用性,测试一个文件流的文件尾标识
	if(feof(fp))
	{
		// 把文件流里的所有未写出数据立刻写出
		fflush(fp);
		printf("\n");
		
		printf("File point have reached the end of file \n");
		sleep(5);
	}
	
	printf("%c", Temp);
}

8. 获取各个挂载点的存储信息操作:

8.1 程序部分:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/vfs.h>	// statfs、fstatfs函数
#include <mntent.h>		// setmntent、getmntent、endmntent函数

char MountPath[] = "/proc/mounts";	// proc系统中的mount文件

struct sStoragreSize
{
	// 容量、使用、可用,单位为 KB
	unsigned int Size_KB;
	unsigned int Used_KB;
	unsigned int Available_KB;
	
	// 挂载标志位:为0,没有挂载;为1,有挂载。
	unsigned char MountFlag;
};

void ReadStoragreSize(char *DevicePath, struct sStoragreSize *pStoragre)
{
	FILE *fpMount = NULL;
	struct mntent *mnt = NULL;
	struct statfs diskInfo;
	
	pStoragre->MountFlag = 0;
	
	// 打开proc系统中的mount文件
	fpMount = setmntent(MountPath, "r");
	if(fpMount == NULL)
	{
		printf("== Open %s error == \n", MountPath);
		return;
	}
	
	// 判断是否有挂载相应的设备,并标记有挂载的设备
	while(1)
	{
		mnt = getmntent(fpMount);
		if(strncmp(mnt->mnt_dir, DevicePath, (sizeof(DevicePath)) - 1) == 0)
		{
			pStoragre->MountFlag = 1;
			break;
		}
	}
	endmntent(fpMount);
	
	if(pStoragre->MountFlag)
	{
		memset(&diskInfo, 0, sizeof(struct statfs));
		statfs(DevicePath, &diskInfo);
		
		// 一定要强制转化为unsigned long long(64位),若地址超过4G,会导致数据溢出
		// 右移10位是为了方便存储数据,只需要用unsigned int(32位)就可以存储
		pStoragre->Size_KB = ((unsigned long long)diskInfo.f_bsize *
							(unsigned long long)diskInfo.f_blocks) >> 10;
		pStoragre->Used_KB = ((unsigned long long)diskInfo.f_bsize *
							(unsigned long long)(diskInfo.f_blocks -
							diskInfo.f_bfree)) >> 10;
		pStoragre->Available_KB = ((unsigned long long)diskInfo.f_bsize *
							(unsigned long long)diskInfo.f_bavail) >> 10;
	}
	else
	{
		pStoragre->Size_KB = pStoragre->Used_KB = pStoragre->Available_KB = 0;
	}
	
}

int main(int argc, char *argv[])
{
	int i = 0;
	char DevicePath[15];
	
	struct sStoragreSize Storagre;
	
	char MachinePath[] = "/home";		// 机身存储的挂载路径
	char SDCardPath[] = "/mnt/sdcard";	// SD卡的挂载路径
	char UdiskPath[] = "/mnt/udisk";	// U盘的挂载路径
	
	for(i = 0; i < 3; i++)
	{
		memset(DevicePath, 0, sizeof(DevicePath));
		memset(&Storagre, 0, sizeof(struct sStoragreSize));
		
		switch(i)
		{
			case 0:
			memcpy(DevicePath, MachinePath, sizeof(MachinePath));
			break;
			
			case 1:
			memcpy(DevicePath, SDCardPath, sizeof(SDCardPath));
			break;
			
			case 2:
			memcpy(DevicePath, UdiskPath, sizeof(UdiskPath));
			break;
		}
		
		ReadStoragreSize(DevicePath, &Storagre);
		if(Storagre.MountFlag)
		{
			printf("DevicePath = %s \n", DevicePath);
			
			printf("Size = %.3f GB \n", ((double)Storagre.Size_KB / (1024 * 1024)));
			printf("Used = %.3f GB \n", ((double)Storagre.Used_KB / (1024 * 1024)));
			printf("Available = %.3f GB \n\n", ((double)Storagre.Available_KB /
										(1024 * 1024)));
		}
		
	}
	
	return 0;
}
8.2 对struct statfs结构体的解析:
// struct statfs结构体位于 /usr/include/i386-linux-gnu/bits/statfs.h

struct statfs
{
	__fsword_t f_type;			// 文件系统类型
	__fsword_t f_bsize;			// 经过优化的传输块大小
#ifndef __USE_FILE_OFFSET64
	__fsblkcnt_t f_blocks;		// 文件系统数据块总数
	__fsblkcnt_t f_bfree;		// 可用块数
	__fsblkcnt_t f_bavail;		// 非超级用户可获取的块数
	__fsfilcnt_t f_files;		// 文件结点总数
	__fsfilcnt_t f_ffree;		// 可用文件结点数
#else
	__fsblkcnt64_t f_blocks;	// 文件系统数据块总数
	__fsblkcnt64_t f_bfree;		// 可用块数
	__fsblkcnt64_t f_bavail;	// 非超级用户可获取的块数
	__fsfilcnt64_t f_files;		// 文件结点总数
	__fsfilcnt64_t f_ffree;		// 可用文件结点数
#endif
	__fsid_t f_fsid;			// 文件系统标识
	__fsword_t f_namelen;		// 文件名的最大长度
	__fsword_t f_frsize;
	__fsword_t f_flags;
	__fsword_t f_spare[4];		// spare for later
};
8.3 对struct stat结构体的解析:
// struct stat结构体位于 /usr/include/i386-linux-gnu/asm/stat.h

#ifdef __i386__

struct stat {
	unsigned long st_dev;			// 文件的设备编号
	unsigned long st_ino;			// 节点
	unsigned short st_mode;			// 文件的类型和存取的读写执行权限
	unsigned short st_nlink;		// 连到该文件的硬连接数目,刚建立的文件值为1
	unsigned short st_uid;			// 用户ID
	unsigned short st_gid;			// 组ID
	unsigned long st_rdev;			// (设备类型)若此文件为设备文件,则为其设备编号
	unsigned long st_size;			// 文件字节数(文件大小)
	unsigned long st_blksize;		// 块大小(文件系统的I/O 缓冲区大小)
	unsigned long st_blocks;		// 文件所占块数
	unsigned long st_atime;			// 最后一次访问时间
	unsigned long st_atime_nsec;	// 
	unsigned long st_mtime;			// 最后一次修改时间
	unsigned long st_mtime_nsec;	// 
	unsigned long st_ctime;			// 最后一次改变时间(指属性)
	unsigned long st_ctime_nsec;	// 
	unsigned long __unused4;		// 
	unsigned long __unused5;		// 
};

#else /* __i386__ */

struct stat {
	__kernel_ulong_t st_dev;
	 __kernel_ulong_t st_ino;
	__kernel_ulong_t st_nlink;

	unsigned int st_mode;
	unsigned int st_uid;
	unsigned int st_gid;
	unsigned int __pad0;
	__kernel_ulong_t st_rdev;
	__kernel_long_t st_size;
	__kernel_long_t st_blksize;
	__kernel_long_t st_blocks;		// 分配的512字节块数。

	__kernel_ulong_t st_atime;
	__kernel_ulong_t st_atime_nsec;
	__kernel_ulong_t st_mtime;
	__kernel_ulong_t st_mtime_nsec;
	__kernel_ulong_t st_ctime;
	__kernel_ulong_t st_ctime_nsec;
	__kernel_long_t __unused[3];
};

#endif
8.4 stat、fstat和lstat函数
stat、fstat和lstat函数:
https://www.cnblogs.com/xj626852095/p/3648237.html
8.5 对比内核态struct kstat结构体和用户态struct stat结构体
# 共同点:
xx_ino			// inode节点号
xx_dev			// 文件的设备ID
xx_mode			// 文件的类型和存取的读写执行权限
xx_nlink		// 连到该文件的硬连接数
xx_uid			// 用户ID
xx_gid			// 组ID
xx_rdev			// 若此文件为设备文件,则为其设备编号
xx_size			// 文件大小,字节为单位
xx_blksize		// 系统块大小(文件系统的I/O 缓冲区大小)
xx_blocks		// 文件所占块数
xx_atime		// 最近访问(access)时间,包含sec和nsec
xx_mtime		// 最近修改(modify)时间,包含sec和nsec
xx_ctime		// 创建(change)时间,包含sec和nsec

# 不同点:
__unused_xx		// 没有使用的

# 总结:
基本上是一样的……

9. 目录操作:待更中……

// 《Linux程序设计 第4版》,Neil Matthew、Richard Stones 著 的第3章与第4章

// 3.7 C语言文件和目录维护

// chmod 系统调用
int chmod(const char *path, mode_t mode);

// chown 系统调用
int chown(const char *path, uid_t owner, gid_t group);

// unlink、link和symlink系统调用
int unlink(const char *path);
int link(const char *path1, const char *path2);
int symlink(const char *path1, const char *path2);

// mkdir和rmdir系统调用
int mkdir(const char *path, mode_t mode);
int rmdir(const char *path);

// chdir系统调用和getcwd函数
int chdir(const char *path);
char *getcwd(char *buf, size_t size);


// 3.8 C语言文件和目录维护

// opendir函数
DIR *opendir(const char *name);

// readdir函数
struct dirent *readdir(DIR *dirp);

// telldir函数
long int telldir(DIR *dirp, long int loc);

// seekdir函数
void seekdir(DIR *dirp);

// closedir函数
int closedir(DIR *dirp);


// 3.9 C语言错误处理

// strerror函数
char *strerror(int errnum);

// perror函数
void perror(const char *s);


// 3.10 /proc文件系统


// 3.11 C语言fcntl和mmap函数
int fcntl(int fildes, int cmd);
int fcntl(int fildes, int cmd, long arg);
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);
int msync(void *addr, size_t len, int flags);
int munmap(void *addr, size_t len);



// 4

// 4.1 int main(int argc, char *argv[])

// 4.1.1 getopt函数
int getopt(int argc, char *const argv[], const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;

// 4.1.1 getopt_long函数
int getopt_long();

// 4.2 环境变量

char *getenv(const char *name);
int putenv(const char *string);

// 4.2.2 environ变量
extern char **environ;


// 4.3 时间和日期
time_t time(time_t *tloc);
double difftime(time_t timel, time_t time2);
struct tm *gmtime(const time_t timeval);
struct tm *localtime(const time_t *timeval);
time_t mktime(struct tm *timeptr);
char *asctime(const struct tm *timeptr);
char *ctime(const time_t *timeval);
size_t strftime(char *s, size_t maxsize, const char *format, struct tm *timeptr);
char *strptime(const char *buf, const char *format, struct tm *timeptr);


// 4.4 临时文件
char *tmpnam(char *s);
FILE *tmpfile(void);
char *mktemp(char *template);
int mkstemp(char *template);


// 4.5 用户信息
uid_t getuid(void);
char *getlogin(void);

struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(const char *name);

void endpwent(void);
struct passwd *getpwent(void);
void setpwent(void);

uid_t geteuid(void);

gid_t getgid(void);
gid_t getegid(void);
int setuid(uid_t uid);
int setgid(gid_t gid);


// 4.6 主机信息
int gethostname(char *name, size_t namelen);
int uname(struct utsname *name);
long gethostid(void);


// 4.7 日志
void syslog(int priority, const char *message, arguments...);
void closelog(void);
void openlog(const char *ident, int logopt, int facility);
int setlogmask(int maskpri);
pid_t getpid(void);
pid_t getppid(void);

// 4.8 资源和限制(硬件的限制、系统策略的限制、具体实现的限制)
int getpriority(int which, id_t who);					// 优先级
int setpriority(int which, id_t who, int priority);
int getrlimit(int resource, struct rlimit *r_limit);	// 限制
int setrlimit(int resource, const struct rlimit *r_limit);
int getrusage(int who, struct rusage *r_usage);			// 资源信息

10. 时间记录:

struct timeval TimeVal_1, TimeVal_2;

gettimeofday(&TimeVal_1, NULL);
// ***** 一顿操作 1 ***** 
// ***** 一顿操作 2 ***** 
// ***** 一顿操作 3 ***** 
gettimeofday(&TimeVal_2, NULL);

double Time1_s = (TimeVal_1.tv_sec) + ((double)TimeVal_1.tv_usec / (1000 * 1000));
double Time2_s = (TimeVal_2.tv_sec) + ((double)TimeVal_2.tv_usec / (1000 * 1000));
double Time1_ms = (TimeVal_1.tv_sec * 1000) + ((double)TimeVal_1.tv_usec / 1000);
double Time2_ms = (TimeVal_2.tv_sec * 1000) + ((double)TimeVal_2.tv_usec / 1000);
	
printf("Running %.2f min (%.2fs %.1f ms) \n\n", (Time2_s - Time1_s) / 60,
									(Time2_s - Time1_s), (Time2_ms - Time1_ms));
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值