Linux系统编程03和04---makefile-gdb-文件IO

  • 作者介绍
  • 学习目标
  • 1 makefile(被make命令解析)
    • 1.1 makefile的基本规则
    • 1.2 makefile工作原理
    • 1.3 makefile中的变量
    • 1.4 模式规则
    • ---版本v2和v3的实例(助于理解)
    • 1.4 makefile函数
    • ---版本v4的实例(助于理解)
    • 1.5 makefile的清理操作
    • ---第五个版本v5增加makefile文件中的清理功能(理解会用即可)
  • 2 gdb调试
    • 2.1 gdb介绍
    • 2.2 启动gdb调试:
    • 2.3 显示源代码
    • 2.4 设置断点
    • 2.5 利用gdb调试代码
    • 2.6 查看变量的值
    • 练习(需要复习视频14 gdb调试综合练习)
  • 3 文件IO
    • 3.1 C库IO函数的工作流程
    • 3.2 库函数与系统函数之间的关系(调用与被调用的关系)
    • 3.3 虚拟地址空间
    • 3.4 pcb和进程管理
    • 3.5 文件IO操作函数
      • 3.5.1 open
      • 3.5.2 close
      • 3.5.3 read
      • 3.5.4 write
      • 3.5.5 lseek
      • 3.5.6 perror和errno
      • 3.5.7 文件操作函数实例
  • 4.文件操作相关函数
    • 4.1 stat/lstat函数
    • 4.2 stat/lstat函数的区别
    • 4.3 目录相关函数
      • 4.3.1 opendir
      • 4.3.2 readdir
      • 4.3.3 closedir
    • 4.4 目录相关函数实例
    • 4.5 dup函数和dup2函数(文件描述符复制)
    • 4.5.1 dup2函数实现文件重定向

作者介绍

张伟伟,男,西安工程大学电子信息学院,2019级硕士研究生,张宏伟人工智能课题组。
微信公众号:可随时查阅,搜索—张二牛的笔记,内容会分类上传。
研究方向:机器视觉与人工智能。
电子邮件:[email protected]
电子邮件:[email protected]

  • 课题组CSDN官方账号,欢迎一键三连: https://blog.csdn.net/m0_37758063/article/details/113527955?spm=1001.2014.3001.5501.

学习目标

熟练使用规则编写简单的makefile文件
熟练使用makefile中的变量
熟练使用makefile中的函数
熟练掌握gdb相关调试指令的使用
了解概念:pcb和文件描述符,虚拟地址空间
熟练掌握Linux系统IO函数的使用

1 makefile(被make命令解析)

makefile文件的命名:makefile 或者 Makefille
makefile文件时用来管理项目工程文件,

1.1 makefile的基本规则

makefile由一组规则组成,规则如下:
目标: 依赖
(tab)命令
makefile基本规则三要素:

  • 目标: 要生成的目标文件
  • 依赖: 目标文件由哪些文件生成
  • 命令: 通过执行该命令由依赖文件生成目标

第一个版本:
makefile文件编写后:

main: mian.c fun1.c fun2.c sum.c
	gcc -o main main.c fun1.c fun2.c sum.c

使用make指令直接调用makefile运行

make

1.2 makefile工作原理

如下图和解释:
Linux系统编程03和04---makefile-gdb-文件IO_第1张图片

若想生成目标, 检查规则中的所有的依赖文件是否都存在:
------如果有的依赖文件不存在, 则向下搜索规则, 看是否有生成该依赖文件的规则:
--------------如果有规则用来生成该依赖文件, 则执行规则中的命令生成依赖文件;
--------------如果没有规则用来生成该依赖文件, 则报错.

------如果所有依赖都存在, 检查规则中的目标是否需要更新, 必须先检查它的所有依赖,依赖中有任何一个被更新, 则目标必须更新.(检查的规则是哪个时间大哪个最新)

  • 若目标的时间 > 依赖的时间, 不更新
  • 若目标的时间 < 依赖的时间, 则更新

总结:

  • 分析各个目标和依赖之间的关系
  • 根据依赖关系自底向上执行命令
  • 根据依赖文件的时间和目标文件的时间确定是否需要更新
    如果目标不依赖任何条件, 则执行对应命令, 以示更新(如:伪目标)

1.3 makefile中的变量

普通变量(自定义字符串)

变量相当于内容替换

变量定义直接用 =
使用变量值用 $(变量名)

下面是变量的定义和使用
foo = abc			// 定义变量并赋值
bar = $(foo)		// 使用变量, $(变量名)

自带变量(一些系统自带的变量名)

CC = gcc #arm-linux-gcc
CPPFLAGS : C预处理的选项 -I
CFLAGS:   C编译器的选项 -Wall -g -c
LDFLAGS :  链接器选项 -L  -l

自动变量(只能在规则中的命令使用))

$@: 表示规则中的目标
$<: 表示规则中的第一个条件
$^: 表示规则中的所有条件, 组成一个列表, 以空格隔开, 如果这个列表中有重复的项则消除重复项。

1.4 模式规则

至少在规则的目标定义中要包含’%’, ‘%’表示一个或多个, 在依赖条件中同样可以使用’%’, 依赖条件中的’%’的取值取决于其目标:

%.o:%.c      前后的百分号必须是相同的字符串

—版本v2和v3的实例(助于理解)

makefile-v2

main:main.o fun1.o fun2.o sum.o
	gcc -o main main.o fun1.o fun2.o sum.o

main.o:main.c
	gcc -o main.o -c main.c -I./

fun1.o:fun1.c
	gcc -o fun1.o -c fun1.c

fun2.o:fun2.c
	gcc -o fun2.o -c fun2.c

sum.o:sum.c
	gcc -o sum.o -c sum.c

makefile-v3

target=main
object=main.o fun1.o fun2.o sum.o
CC=gcc
CPPFLAGS=-I./

$(target):$(object)
	$(CC) -o $@ $^

%.o:%.c 
	$(CC) -o $@ -c $< $(CPPFLAGS) 

#main.o:main.c
#	gcc -o main.o -c main.c -I./
#
#fun1.o:fun1.c
#	gcc -o fun1.o -c fun1.c
#
#fun2.o:fun2.c
#	gcc -o fun2.o -c fun2.c
#
#sum.o:sum.c
#	gcc -o sum.o -c sum.c

1.4 makefile函数

makefile中的函数有很多,在这里给大家介绍两个常用的
匹配替换函数

1. wildcard-查找指定目录下指定类型的文件
	src=$(wildcard *.c)  //找到当前目录下所有后缀为.c的文件,赋值给src
2. patsubst – 匹配替换
	obj=$(patsubst %.c,%.o, $(src)) //把src变量里所有后缀为.c的文件替换成.o
---------例如------------
当前目录下有main.c fun1.c fun2.c sum.c
src=$(wildcard *.c) 等价于src=main.c fun1.c fun2.c sum.c
obj=$(patsubst %.c,%.o, $(src))等价于obj=main.o fun1.o fun2.o sum.o

—版本v4的实例(助于理解)

target=main
src=$(wildcard ./*.c)
object=$(patsubst %.c,%.o, $(src))
CC=gcc
CPPFLAGS=-I./

$(target):$(object)
	$(CC) -o $@ $^
	
%.o:%.c
	$(CC) -o $@ -c $< $(CPPFALGS)

缺点: 每次重新编译都需要手工清理中间.o文件和最终目标文件

1.5 makefile的清理操作

第一个出现的目标就是终极目标

.PHONY:clean
clean:
	rm -f $(objects) $(target)

—第五个版本v5增加makefile文件中的清理功能(理解会用即可)

src=$(wildcard ./*.c)
object=$(patsubst %.c, %.o, $(src))
target=main
CC=gcc
CPPFLAGS=-I./

$(target):$(object)
	$(CC) -o $@ $^

%.o:%.c
	$(CC) -o $@ -c $< $(CPPFLAGS) 

.PHONY:clean
clean:
	-rm -f $(target) $(object)

@不显示执行过程 加-不影响后续程序运行

2 gdb调试

2.1 gdb介绍

GDB(GNU Debugger)是GCC的调试工具。
功能:
 启动程序, 可以按照你的自定义的要求随心所欲的运行程序。
 可让被调试的程序在你所指定的断点处停住。(断点可以是条件表达式)
 当程序被停住时, 可以检查此时你的程序中所发生的事。
 动态的改变你程序的执行环境。

gdb是在程序运行的结果与预期不符合的时候,可以使用gdb进行调试,
特别注意的是:使用gdb调试需要在编译的时候加上-g参数。
gcc -g -c hello.c
gcc -o hello hello.o

2.2 启动gdb调试:

gdb  可执行程序

设置程序运行参数
set args 可指定运行时参数。(如:set args 10 20 30 40 50 )
show args 命令可以查看设置好的运行参数
启动程序
	run:程序开始执行, 如果有断点, 停在第一个断点处
	start:程序向下执行一行。(在第一条语句处停止)

2.3 显示源代码

	list:默认显示10行,回车继续显示   
如 list 1   list main  list - 

list linenum:打印第linenum行的上下文内容.
list function:显示函数名为function的函数的源程序。
list: 显示当前行后面的源程序。
list -:显示当前文件开始处的源程序。
list file:linenum: 显示file文件下第n行
list file:function: 显示file文件的函数名为function的函数的源程序
 一般是打印当前行的上5行和下5行, 如果显示函数是是上2行下8行, 默认是10行, 当然, 你也可以定制显示的范围, 使用下面命令可以设置一次显示源程序的行数。
set listsize count:设置一次显示源代码的行数。         
show listsize:   查看当前listsize的设置

2.4 设置断点

break   #打断点
info    #查看断点
b 10    #在第十行打断点
i b     #查看断点
next    #下一步运行
disable 1   disable 2-4   #使断点失效
enable 1 enable 2-4  enable 1 3  使断点有效
delete 1  #删除断点
b test.c:8 if intValue==5 当值变量intValue断点的值为5时断点生效

2.5 利用gdb调试代码

维护断点

run 运行程序
next 单步跟踪  

step 单步跟踪  (可以进函数内部)
finish 退出进入的函数

until 挑出函数体
quit 退出gdb调试
continue 继续运行程序,可简写为c(若有断点,则跳转到下一个断点)

2.6 查看变量的值

print &i 
p &i
p i

display 变量名   #自动显示变量的值
disable display 1 2

练习(需要复习视频14 gdb调试综合练习)

3 文件IO

我们要使用IO操作函数,则需要先了解内核的工作原理,他对于底层做了一定的封装。

3.1 C库IO函数的工作流程

Linux系统编程03和04---makefile-gdb-文件IO_第2张图片

  • 文件描述符: 通过文件描述可以找到文件的inode, 通过inode可以找到对应的数据块
  • 文件指针: 读和写共享一个文件指针, 读或者写都会引起文件指针的变化
  • 文件缓冲区: 读或者写会先通过文件缓冲区, 主要目的是为了减少对磁盘的读写次数, 提高读写磁盘的效率.(写内存快)

备注:
头文件stdio.h 的第48行处: typedef struct _IO_FILE FILE;
头文件libio.h 的第241行处: struct _IO_FILE, 这个接头体定义中有一个_fileno成员, 这个就是文件描述符
Linux系统编程03和04---makefile-gdb-文件IO_第3张图片

3.2 库函数与系统函数之间的关系(调用与被调用的关系)

库函数是对系统函数的进一步封装
Linux系统编程03和04---makefile-gdb-文件IO_第4张图片

3.3 虚拟地址空间

Linux系统编程03和04---makefile-gdb-文件IO_第5张图片
进程的虚拟地址空间分为用户区和内核区, 其中内核区是受保护的, 用户是不能够对其进行读写操作的;

内核区中很重要的一个就是进程管理, 进程管理中有一个区域就是PCB(本质是一个结构体);

PCB中有文件描述符表, 文件描述符表中存放着打开的文件描述符, 涉及到文件的IO操作都会用到这个文件描述符.

3.4 pcb和进程管理

pcb就是一个结构体
Linux系统编程03和04---makefile-gdb-文件IO_第6张图片
虚拟地址空间-》内核区-》PCB-》文件描述表-》文件描述符-》文件IO操作使用文件描述符

3.5 文件IO操作函数

3.5.1 open

	函数描述: 打开或者新建一个文件
	函数原型:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
	函数参数:
	pathname参数是要打开或创建的文件名,和fopen一样, pathname既可以是相对路径也可以是绝对路径。 
	flags参数有一系列常数值可供选择, 可以同时选择多个常数用按位或运算符连接起来, 所以这些常数的宏定义都以O_开头,表示or。
	必选项:以下三个常数中必须指定一个, 且仅允许指定一个。
	O_RDONLY 只读打开
	O_WRONLY 只写打开
	O_RDWR 可读可写打开
	以下可选项可以同时指定0个或多个, 和必选项按位或起来作为flags参数。可选项有很多, 这里只介绍几个常用选项:
	O_APPEND 表示追加。如果文件已有内容, 这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容。
	O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode, 表示该文件的访问权限。

	文件最终权限:mode & ~umask
	O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。
	O_TRUNC 如果文件已存在, 将其长度截断为为0字节。
	O_NONBLOCK 对于设备文件, 以O_NONBLOCK方式打开可以做非阻塞I/O(NonblockI/O),非阻塞I/O。
	函数返回值:
	成功: 返回一个最小且未被占用的文件描述符
	失败: 返回-1, 并设置errno值.

3.5.2 close

close函数
	函数描述: 关闭文件
	函数原型:  int close(int fd);
	函数参数:  fd文件描述符
	函数返回值:
	成功返回0
	失败返回-1, 并设置errno值.

3.5.3 read

  • 读普通文件内容之后再次读,则read函数会立刻返回,表明read函数读普通文件时非阻塞的。
  • 读设备文件是阻塞的。例如(/dev/tty–终端设备是阻塞的) STDIN_FILENO
    阻塞和非阻塞是文件的属性还是read函数的属性
	函数描述: 从打开的设备或文件中读取数据
	函数原型: ssize_t read(int fd, void *buf, size_t count);
	函数参数:
	fd: 文件描述符
	buf: 读上来的数据保存在缓冲区buf中
	count: buf缓冲区存放的最大字节数
	函数返回值:>0:读取到的字节数
	=0:文件读取完毕
	-1: 出错,并设置errno

3.5.4 write

	函数描述: 向打开的设备或文件中写数据
	函数原型: ssize_t write(int fd, const void *buf, size_t count);
	函数参数: 
	fd:文件描述符
	buf:缓冲区,要写入文件或设备的数据
	count:buf中数据的长度
	函数返回值:
	成功:返回写入的字节数
	错误:返回-1并设置errno

3.5.5 lseek

所有打开的文件都有一个当前文件偏移量(current file offset),以下简称为cfo

#include 
#include 
off_t lseek(int fd, off_t offset, int whence);

	函数描述: 移动文件指针
	函数原型: off_t lseek(int fd, off_t offset, int whence);
	函数参数:
	fd:文件描述符
	参数 offset 的含义取决于参数 whence:
	如果 whence 是 SEEK_SET,文件偏移量将设置为 offset。
	如果 whence 是 SEEK_CUR,文件偏移量将被设置为 cfo 加上 offset,offset 可以为正也可以为负。
	如果 whence 是 SEEK_END,文件偏移量将被设置为文件长度加上 offset,offset 可以为正也可以为负。
	函数返回值: 若lseek成功执行, 则返回新的偏移量。

fseek使用的一些例子

	lseek函数常用操作
	文件指针移动到头部
lseek(fd, 0, SEEK_SET);
	获取文件指针当前位置
int len = lseek(fd, 0, SEEK_CUR);
	获取文件长度
int len = lseek(fd, 0, SEEK_END);
	lseek实现文件拓展
off_t currpos;
// 从文件尾部开始向后拓展1000个字节
currpos = lseek(fd, 1000, SEEK_END); 
// 额外执行一次写操作,否则文件无法完成拓展
write(fd, “a”, 1);	// 数据随便写

3.5.6 perror和errno

errno是一个全局变量, 当系统调用后若出错会将errno进行设置, perror可以将errno对应的描述信息打印出来.
如:perror(“open”); 如果报错的话打印: open:(空格)错误信息。

3.5.7 文件操作函数实例

Linux系统编程03和04---makefile-gdb-文件IO_第7张图片Linux系统编程03和04---makefile-gdb-文件IO_第8张图片

4.文件操作相关函数

4.1 stat/lstat函数

	函数描述: 获取文件属性
	函数原型: int stat(const char *pathname, struct stat *buf);
         int lstat(const char *pathname, struct stat *buf);
	函数返回值: 
	成功返回 0
	失败返回 -1
	struct stat {
	    dev_t          st_dev;        //文件的设备编号
	    ino_t           st_ino;        //节点
	    mode_t         st_mode;      //文件的类型和存取的权限
	    nlink_t         st_nlink;     //连到该文件的硬连接数目,刚建立的文件值为1
	    uid_t           st_uid;       //用户ID
	    gid_t           st_gid;       //组ID
	    dev_t          st_rdev;      //(设备类型)若此文件为设备文件,则为其设备编号
	    off_t          st_size;      //文件字节数(文件大小)
	    blksize_t       st_blksize;   //块大小(文件系统的I/O 缓冲区大小)
	    blkcnt_t        st_blocks;    //块数
	    time_t         st_atime;     //最后一次访问时间
	    time_t         st_mtime;     //最后一次修改时间
	    time_t         st_ctime;     //最后一次改变时间(指属性)

简单示例:

struct stat st;
int Just =  stat(args[1], &st);
printf("[%d,%d,%d,%d,%d]",st.st_uid, st.st_gid, st.st_size,st.st_mtime);

互斥用if else与有可能用if,判断文件权限使用&的方法,与位去比。

//stat函数测试: 获取文件类型和权限
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
	//int stat(const char *pathname, struct stat *buf);
	//获取文件属性
	struct stat sb;
	stat(argv[1], &sb);

	//获取文件类型
	if ((sb.st_mode & S_IFMT) == S_IFREG) 
 	{
		printf("普通文件\n");
	}	
	else if((sb.st_mode & S_IFMT) ==S_IFDIR)
	{
		printf("目录文件\n");
	}
	else if((sb.st_mode & S_IFMT) ==S_IFLNK)
	{
		printf("连接文件\n");
	}
	

	if (S_ISREG(sb.st_mode)) 
	{
	 	printf("普通文件\n");
	}
	else if(S_ISDIR(sb.st_mode))
	{
		printf("目录文件\n");
	}
	else if(S_ISLNK(sb.st_mode))
	{
		printf("连接文件\n");
	}

	//判断文件权限
	if(sb.st_mode & S_IROTH)
	{
		printf("---R----");
	}

	if(sb.st_mode & S_IWOTH)
	{
		printf("---W----");
	}
	
	if(sb.st_mode & S_IXOTH)
	{
		printf("---X----");
	}

	printf("\n");

	return 0;
}

4.2 stat/lstat函数的区别

stat函数和lstat函数的区别
 对于普通文件, 这两个函数没有区别, 是一样的.
 对于连接文件,调用lstat函数获取的是链接文件本身的属性信息;
而stat函数获取的是链接文件指向的文件的属性信息

4.3 目录相关函数

Linux系统编程03和04---makefile-gdb-文件IO_第9张图片

4.3.1 opendir

opendir函数
	函数描述:打开一个目录 
	函数原型: DIR *opendir(const char *name);
	函数返回值: 指向目录的指针
	函数参数: 要遍历的目录(相对路径或者绝对路径)

4.3.2 readdir

readdir函数
	函数描述: 读取目录内容--目录项
	函数原型: struct dirent *readdir(DIR *dirp);
	函数返回值: 读取的目录项指针
	函数参数: opendir函数的返回值
struct dirent
{
  ino_t d_ino;             // 此目录进入点的inode
  off_t d_off;              // 目录文件开头至此目录进入点的位移
  signed short int d_reclen;   // d_name 的长度, 不包含NULL 字符
  unsigned char d_type;     // d_name 所指的文件类型 
  char d_name[256];	    // 文件名
};

d_type的取值: 
	DT_BLK - 块设备
	DT_CHR - 字符设备
	DT_DIR - 目录
	DT_LNK - 软连接
	DT_FIFO - 管道
	DT_REG - 普通文件
	DT_SOCK - 套接字
	DT_UNKNOWN - 未知

4.3.3 closedir

closedir函数
	函数描述: 关闭目录
	函数原型: int closedir(DIR *dirp);
	函数返回值: 成功返回0, 失败返回-1
	函数参数: opendir函数的返回值

4.4 目录相关函数实例

1.打开目录 opendir
2.循环读目录 readdir
3.关闭目录 closedir

//目录操作测试:opendit readdir closedir
#include
#include
#include

#include
#include

#include

int main(int argc, char *argv[])
{
	//打开目录
	//DIR *opendir(const char *name);
	DIR *pDir = opendir(argv[1]);
	if(pDir==NULL)
	{
		perror("opendir error");
		return -1;
	}
	//循环读取目录项
	//struct dirent *readdir(DIR *dirp);
	struct dirent *pDent  = NULL;
	while((pDent = readdir(pDir))!=NULL)
	{
		//过滤掉. 和 ..
		if(strcmp(pDent->d_name,".")==0 || strcmp(pDent->d_name,"..")==0)
		{
			continue;
		}
		printf("[%s]--->",pDent->d_name);
		switch(pDent->d_type)
		{
			case DT_REG:
				printf("普通文件\n");
				break;
			case DT_DIR:
				printf("目录文件\n");
				break;
			case DT_LNK:
				printf("链接文件\n");
				break;
			default:
				printf("未知文件\n");
		}
	}
	//关闭目录
	closedir(pDir);
}

4.5 dup函数和dup2函数(文件描述符复制)

  • 函数原型
//dup函数
	函数描述: 复制文件描述符
	函数原型: int dup(int oldfd);
	函数参数: oldfd -要复制的文件描述符
	函数返回值:
	成功: 返回最小且没被占用的文件描述符
	失败: 返回-1, 设置errno值
//dup2函数
	函数描述: 复制文件描述符
	函数原型: int dup2(int oldfd, int newfd);
newfd 随着 oldfd 变。
	函数参数: 
	oldfd-原来的文件描述符
	newfd-复制成的新的文件描述符
	函数返回值:
	成功: 将oldfd复制给newfd, 两个文件描述符指向同一个文件
	失败: 返回-1, 设置errno值
	假设newfd已经指向了一个文件,首先close原来打开的文件,然后newfd指向oldfd指向的文件.
若newfd没有被占用,newfd指向oldfd指向的文件

使用实例:

//判断是否指向同一文件,使用fd去写入,用newfd去读
int newfd = dup(fd);

//使用fd对文件进行写操作
write(fd, "hello world", strlen("hello world"));

//使用newfd读文件
char buf[64];
memset(buf, 0x00, sizeof(buf));
int n = read(newfd, buf, sizeof(buf));
printf("read over: n==[%d], buf==[%s]\n", n, buf);

视频16 17 18未看

4.5.1 dup2函数实现文件重定向

理解文件重定向的原理:
Linux系统编程03和04---makefile-gdb-文件IO_第10张图片# 4.6 fcntl函数讲解(改变文件flags标识)

	函数描述: 改变已经打开的文件的属性
	函数原型: int fcntl(int fd, int cmd, ... /* arg */ );
	若cmd为F_DUPFD, 复制文件描述符, 与dup相同
	若cmd为F_GETFL, 获取文件描述符的flag属性值
	若cmd为 F_SETFL, 设置文件描述符的flag属性
	函数返回值:返回值取决于cmd
	成功
	若cmd为F_DUPFD, 返回一个新的文件描述符
	若cmd为F_GETFL, 返回文件描述符的flags值
	若cmd为 F_SETFL, 返回0
	失败返回-1, 并设置errno值.

	fcntl函数常用的操作:
1 复制一个新的文件描述符:
int newfd = fcntl(fd, F_DUPFD, 0);
2 获取文件的属性标志
int flag = fcntl(fd, F_GETFL, 0)
3 设置文件状态标志
flag = flag | O_APPEND;
fcntl(fd, F_SETFL, flag)
4 常用的属性标志
O_APPEND-----设置文件打开为末尾添加
O_NONBLOCK-----设置打开的文件描述符为非阻塞
  • 注意:获取文件的flags属性并修改固定的三句(掌握即可)
//获得和设置fd的flags属性
	int flags = fcntl(fd, F_GETFL, 0);
	flags = flags | O_APPEND;
	fcntl(fd, F_SETFL, flags);

你可能感兴趣的:(Linux操作系统,linux,bash,运维)