Linux系统编程 Makefile-IO

举报
xcc-2022 发表于 2022/09/29 17:10:25 2022/09/29
【摘要】 @[toc] 01. 学习目标熟练使用规则编写简单的makefile文件熟练使用makefile中的变量熟练使用makefile中的函数了解概念: pcb和文件描述符,虚拟地址空间了解Linux系统常用IO函数(暂不要求能够使用)熟练掌握Linux系统IO函数的使用 02. Makefile简介一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列...

@[toc]

01. 学习目标

  • 熟练使用规则编写简单的makefile文件
  • 熟练使用makefile中的变量
  • 熟练使用makefile中的函数
  • 了解概念: pcb和文件描述符,虚拟地址空间
  • 了解Linux系统常用IO函数(暂不要求能够使用)
  • 熟练掌握Linux系统IO函数的使用

02. Makefile简介

一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令

Makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,[Visual C++](https://baike.baidu.com/item/Visual C%2B%2B)的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。

make主要解决两个问题:

1) 大量代码的关系维护

大项目中源代码比较多,手工维护、编译时间长而且编译命令复杂,难以记忆及维护

把代码维护命令及编译命令写在makefile文件中,然后再用make工具解析此文件自动执行相应命令,可实现代码的合理编译

2) 减少重复编译时间

n 在改动其中一个文件的时候,能判断哪些文件被修改过,可以只对该文件进行重新编译,然后重新链接所有的目标文件,节省编译时间

Makefile文件命名规则

makefile和Makefile都可以,推荐使用Makefile。

make工具的安装

sudo apt install make

03. Makefile语法规则

一条规则:

目标:依赖文件列表

命令列表

add.o:add.c
	gcc -c add.c -o add.o

Makefile基本规则三要素:

1)目标:

  • 通常是要产生的文件名称,目标可以是可执行文件或其它obj文件,也可是一个动作的名称(clean)

2)依赖文件:

  • 用来输入从而产生目标的文件
  • 一个目标通常有几个依赖文件(可以没有)

3)命令:

  • make执行的动作,一个规则可以含几个命令(可以没有)
  • 有多个命令时,每个命令占一行

举例说明:

测试代码:(注意换行)

all:test1 test2
	echo "hello all"

test1:
	echo "test1"

test2:
	echo "test2"

image-20220921204235867

04. make命令格式

make是一个命令工具,它解释Makefile 中的指令(应该说是规则)。

make命令格式:

make [ -f file ][ options ][ targets ]

1.[ -f file ]:

  • make默认在工作目录中寻找名为GNUmakefile、makefile、Makefile的文件作为makefile输入文件
  • -f 可以指定以上名字以外的文件作为makefile输入文件

l

2.[ options ]

  • -v: 显示make工具的版本信息
  • -w: 在处理makefile之前和之后显示工作路径
  • -C dir:读取makefile之前改变工作路径至dir目录
  • -n:只打印要执行的命令但不执行image-20220921204422106
  • -s:执行但不显示执行的命令

3.[ targets ]:

  • 若使用make命令时没有指定目标,则make工具默认会实现makefile文件内的第一个目标,然后退出
  • 指定了make工具要实现的目标,目标可以是一个或多个(多个目标间用空格隔开)。

示例:

1527734864437

1527735007592

05. Makefile示例

测试程序: test.c add.c sub.c mul.c div.c

image-20220921205941683

5.1 最简单的Makefile

test:test.c add.c sub.c mul.c div.c
    gcc test.c add.c sub.c mul.c div.c -o test

image-20220921210529351

缺点:效率低,修改一个文件,所有文件会被全部编译

5.2 第二个版本Makefile

test:add.o sub.o mul.o div.o test.o 
	gcc add.o sub.o mul.o div.o test.o -o test

add.o:add.c
	gcc -c add.c -o add.o
sub.o:sub.c
	gcc -c sub.c -o sub.o
mul.o:mul.c
	gcc -c mul.c -o mul.o
div.o:div.c
	gcc -c div.c -o div.o
test.o:test.c
	gcc -c test.c -o test.o

image-20220921212622547

修改add.c之后,只运行了add.c依赖,可对比上图输出结果

image-20220921212926552

image-20220921213747210

代码重复,有点鸡肋,接下来的我们使用变量再改进

06. Makefile中的变量

在Makefile中使用变量有点类似于C语言中的宏定义,使用该变量相当于内容替换,使用变量可以使Makefile易于维护,修改内容变得简单变量定义及使用。

6.1 自定义变量

1)定义变量方法:

变量名=变量值

2)引用变量:

( 变量名 ) (变量名)或 {变量名}

3)makefile的变量名:

  • makefile变量名可以以数字开头
  • 变量是大小写敏感的
  • 变量一般都在makefile的头部定义
  • 变量几乎可在makefile的任何地方使用

示例:

image-20220921215028596

我们再写一个规则,为了自动清除.o文件和可执行文件test:

clean:
	rm -rf $(VAR) test

image-20220921215622873

除了使用用户自定义变量,makefile中也提供了一些变量(变量名大写)供用户直接使用,我们可以直接对其进行赋值。

CC = gcc #arm-linux-gcc

CPPFLAGS : C预处理的选项 如:-I

CFLAGS: C编译器的选项 -Wall -g -c

LDFLAGS : 链接器选项 -L -l

6.2 自动变量

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

注意:自动变量只能在规则的命令中中使用

参考示例:

image-20220921221548412

image-20220921222503897

怎么感觉没什么区别,一样需要不断地copy,能不能再偷懒?答:可以的,我们往下看。

6.3 模式规则

模式规则示例:

#模式匹配 所有的.o都依赖对应的.c
#将所有的.c 生成对应的.o

%.o:%.c
	gcc  -c $< -o $@

Makefile第三个版本:

image-20220921223112264

要是几百个.o文件,那第一行也得写一百个.o,能不能再偷懒? 答:可以,我们用函数解决。

07. Makefile中的函数

makefile中的函数有很多,在这里给大家介绍两个最常用的。

  1. wildcard – 查找指定目录下的指定类型的文件

src = $(wildcard *.c) //找到当前目录下所有后缀为.c的文件,赋值给src

  1. patsubst – 匹配替换

obj = $(patsubst %.c,%.o, $(src)) //把src变量里所有后缀为.c的文件替换成.o==(这里不能用* , %会把对应的.c文件变成add.o)==

在makefile中所有的函数都是有返回值的。

Makefile第四个版本:

#获取当前目录下所有的.c文件
SRC=$(wildcard ./*.c)

#将SRC中所有出现.c的替换成.o
VAR=$(patsubst %.c, %.o, $(SRC))

TARGET=test
$(TARGET):$(VAR)
    gcc $(VAR) -o $(TARGET) 

%.o:%.c
    gcc -c $< -o $@

image-20220921230728733

还不够完美,当我们新建一个文件为clena,无法make clean,也就无法清除了。

image-20220921231707106

为了解决重命名问题,引申出伪目标

08. Makefile中的伪目标

clean用途: 清除编译生成的中间.o文件和最终目标文件

make clean 如果当前目录下有同名clean文件,则不执行clean对应的命令,解决方案:

Ø 伪目标声明: .PHONY:clean

声明目标为伪目标之后,makefile将不会该判断目标是否存在或者该目标是否需要更新

#无条件执行命令
.PHONY:clean
clean:
-rm -rf $(OBJS) $(TARGET)

clean命令中的特殊符号:

  • ==“-”==此条命令出错,make也会继续执行后续的命令。如:“-rm main.o”
  • ==“@”==不显示命令本身,只显示结果。如:“@echo clean done”

%.o:%.c
@gcc -c $< -o $@
clean:
-rm -rf $(OBJS) $(TARGET)

Makefile第五个版本:

#声明clean为伪目标 伪目标不去判断目标文件是否存在或者已经更新 
#无条件执行命令
SRC=$(wildcard *.c)
OBJS=$(patsubst %.c, %.o, $(SRC))
TARGET=testVAR
$(TARGET):$(VAR)
    gcc $(OBJS) -o $(TARGET) 

%.o:%.c
    gcc -c $< -o $@
.PHONY:clean
clean:
    rm -rf $(VAR) $(TARGET)

总结: 一条规则,两个函数,三个变量。

09. Makefile工作原理

1)若想生成目标, 检查规则中的依赖条件是否存在,如不存在,则寻找是否有规则用来 生成该依赖文件

image-20220922074320872

2) 检查规则中的目标是否需要更新,必须先检查它的所有依赖,依赖中有任一个被更新,则目标必须更新

image-20220922073919760

总结:

Ø 分析各个目标和依赖之间的关系

Ø 根据依赖关系自底向上执行命令

Ø 根据修改时间比目标新,确定更新

Ø 如果目标不依赖任何条件,则执行对应命令,以示更新

10. 系统调用简介和实现

10.1 什么是系统调用

系统调用,顾名思义,说的是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件,可以通过时钟相关的系统调用获得系统时间或设置定时器等。

从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。

1527649958892

系统服务之所以需要通过系统调用来提供给用户空间的根本原因是为了对系统进行“保护”,因为我们知道 Linux 的运行空间分为内核空间与用户空间,它们各自运行在不同的级别中,逻辑上相互隔离。

所以用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间函数。

比如我们熟悉的“hello world”程序(执行时)就是标准的用户空间进程,它使用的打印函数 printf 就属于用户空间函数,打印的字符“hello word”字符串也属于用户空间数据。

但是很多情况下,用户进程需要获得系统服务(调用系统程序),这时就必须利用系统提供给用户的“特殊接口”——系统调用了,它的特殊性主要在于规定了用户进程进入内核的具体位置。

换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限制才能保证内核安全无误。我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园,但你必须老老实实地坐在观光车上,按照规定的路线观光游览。当然,不准下车,因为那样太危险,不是让你丢掉小命,就是让你吓坏了野生动物。

10.2 系统调用的实现

系统调用是属于操作系统内核的一部分的,必须以某种方式提供给进程让它们去调用。CPU 可以在不同的特权级别下运行,而相应的操作系统也有不同的运行级别,用户态和内核态。运行在内核态的进程可以毫无限制的访问各种资源,而在用户态下的用户进程的各种操作都有着限制,比如不能随意的访问内存、不能开闭中断以及切换运行的特权级别。显然,属于内核的系统调用一定是运行在内核态下,但是如何切换到内核态呢?

答案是==软件中断==。软件中断和我们常说的中断(硬件中断)不同之处在于,它是通过软件指令触发而并非外设引发的中断,也就是说,又是编程人员开发出的一种异常(该异常为正常的异常)。操作系统一般是通过软件中断从用户态切换到内核态。

11. 系统调用和库函数的区别

Linux 下对文件操作有两种方式:系统调用(system call)库函数调用(Library functions)

库函数由两类函数组成:

1)不需要调用系统调用

不需要切换到内核空间即可完成函数全部功能,并且将结果反馈给应用程序,如strcpy、bzero 等字符串操作函数。

2)需要调用系统调用

需要切换到内核空间,这类函数通过封装系统调用去实现相应功能,如 printf、fread等。

1527650321383

系统调用是需要时间的,程序中频繁的使用系统调用会降低程序的运行效率。当运行内核代码时,CPU工作在内核态,在系统调用发生前需要保存用户态的栈和内存环境,然后转入内核态工作。系统调用结束后,又要切换回用户态。这种环境的切换会消耗掉许多时间 。

12. C库中IO函数工作流程

1527650554264

库函数访问文件的时候根据需要,设置不同类型的缓冲区,从而减少了直接调用 IO 系统调用的次数,提高了访问效率。

这个过程类似于快递员给某个区域(内核空间)送快递一样,快递员有两种方式送:

1)来一件快递就马上送到目的地,来一件送一件,这样导致来回走比较频繁(系统调用)

2)等快递攒着差不多后(缓冲区),才一次性送到目的地(库函数调用)

13. 错误处理函数

==errno== 是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。查看错误代码errno是调试程序的一个重要方法。

当Linux C API函数发生异常时,一般会将errno全局变量赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因。

测试程序:

#include <stdio.h>  //fopen
#include <errno.h>  //errno
#include <string.h> //strerror(errno)

//errno是一个全局变量,在errno.h头文件中有定义
//erron是保存系统最近的出错错误码

int main()
{
    //打开文件
    FILE *fp = fopen("xxxx", "r");
    if (NULL == fp)
    {
        printf("%d\n", errno);  //打印错误码
        printf("%s\n", strerror(errno)); //根据errno的数字转换成相应的文字
        //与上面等同
        perror("fopen err");    //打印错误原因的字符串
    }

    return 0;
}

错误码返回值

printf("0代表%s\n",strerror(0));
printf("1代表%s\n",strerror(1));
printf("2代表%s\n",strerror(2));
printf("3代表%s\n",strerror(3));

image-20220922092849601

查看错误号

/usr/include/asm-generic/errno-base.h

/usr/include/asm-generic/errno.h

1527755557333

14. 虚拟地址空间

每个进程都会分配虚拟地址空间,在32位机器上,该地址空间为4G 。

1527650975663

在进程里平时所说的指针变量,保存的就是虚拟地址。当应用程序使用虚拟地址访问内存时,处理器(CPU)会将其转化成物理地址(MMU)。

MMU:将虚拟的地址转化为物理地址。

这样做的好处在于:

  • 进程隔离,更好的保护系统安全运行
  • 屏蔽物理差异带来的麻烦,方便操作系统和编译器安排进程地址

15. 文件描述符

在 Linux 的世界里,一切设备皆文件。我们可以系统调用中 I/O 的函数(I:input,输入;O:output,输出),对文件进行相应的操作( open()、close()、write() 、read() 等)。

打开现存文件或新建文件时,系统(内核)会返回一个文件描述符,文件描述符用来指定已打开的文件。这个文件描述符相当于这个已打开文件的标号,文件描述符是非负整数,是文件的标识,操作这个文件描述符相当于操作这个描述符所指定的文件。

程序运行起来后(每个进程)都有一张文件描述符的表,标准输入、标准输出、标准错误输出设备文件被打开,对应的文件描述符 0、1、2 记录在表中。程序运行起来后这三个文件描述符是默认打开的。

#define STDIN_FILENO  0 //标准输入的文件描述符
#define STDOUT_FILENO 1 //标准输出的文件描述符
#define STDERR_FILENO 2 //标准错误的文件描述符

在程序运行起来后打开其他文件时,系统会返回文件描述符表中最小可用的文件描述符,并将此文件描述符记录在表中。

1527651181126

最大打开的文件个数

Linux 中一个进程最多只能打开 NR_OPEN_DEFAULT (即1024)个文件,故当文件不再使用时应及时调用 close() 函数关闭文件。

  • 查看当前系统允许打开最大文件个数:

    cat /proc/sys/fs/file-max

  • 当前默认设置最大打开文件个数1024

    ulimit -a

    image-20220922103323377

  • 修改默认设置最大打开文件个数为4096

    ulimit -n 4096

16. 常用文件IO函数

16.1 open函数

image-20220922114956455

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
功能:
    打开文件,如果文件不存在则可以选择创建。
参数:
    pathname:文件的路径及文件名
    flags:打开文件的行为标志,必选项 O_RDONLY, O_WRONLY, O_RDWR
    mode:这个参数,只有在文件不存在时有效,指新建文件时指定文件的权限
返回值:
    成功:成功返回打开的文件描述符
    失败:-1

flags详细说明

必选项:

取值 含义
O_RDONLY 以只读的方式打开
O_WRONLY 以只写的方式打开
O_RDWR 以可读、可写的方式打开

可选项,和必选项按位或起来

取值 含义
O_CREAT 文件不存在则创建文件,使用此选项时需使用mode说明文件的权限
O_EXCL 如果同时指定了O_CREAT,且文件已经存在,则出错
O_TRUNC 如果文件存在,则清空文件内容
O_APPEND 写文件时,数据添加到文件末尾
O_NONBLOCK 对于设备文件, 以O_NONBLOCK方式打开可以做非阻塞I/O

mode补充说明

  1. 文件最终权限:mode & ~umask

  2. shell进程的umask掩码可以用umask命令查看

Ø umask:查看掩码(补码)

Ø umask mode:设置掩码,mode为八进制数

Ø umask -S:查看各组用户的默认操作权限

取值 八进制 含义
S_IRWXU 00700 文件所有者的读、写、可执行权限
S_IRUSR 00400 文件所有者的读权限
S_IWUSR 00200 文件所有者的写权限
S_IXUSR 00100 文件所有者的可执行权限
S_IRWXG 00070 文件所有者同组用户的读、写、可执行权限
S_IRGRP 00040 文件所有者同组用户的读权限
S_IWGRP 00020 文件所有者同组用户的写权限
S_IXGRP 00010 文件所有者同组用户的可执行权限
S_IRWXO 00007 其他组用户的读、写、可执行权限
S_IROTH 00004 其他组用户的读权限
S_IWOTH 00002 其他组用户的写权限
S_IXOTH 00001 其他组用户的可执行权限

示例:

#include <stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
    int fd = -1;

    //1.以只读方式打开一个文件  如果文件不存在就报错
    //fd = open("txt", O_RDONLY);
    //2.以只写的方式打开一个文件 如果文件不存在就报错
    //fd = open("txt", O_WRONLY);
    //3.以只写的方式打开一个文件 如果文件不存在就创建, 如果文件存在就直接打开
    //fd = open("txt", O_WRONLY | O_CREAT, 0644);
    //4.以只读的方式打开一个文件 如果文件不存在就创建
    //fd = open("txt", O_RDONLY | O_CREAT, 0644);
    //5.以读写的方式打开文件 如果文件存在就报错, 如果文件不存在就创建
    //fd = open("txt", O_RDWR | O_CREAT | O_EXCL, 0644);
    //6.以读写的方式打开一个文件 如果文件不存在就创建 如果文件存在就清零
    fd = open("txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (-1 == fd)
    {
        perror("open"); 
        return 1;
    }
    
    printf("fd = %d\n",fd);
    printf("打开文件成功....\n");
    //关闭文件
    close(fd);c

    return 0;
}

16.2 close函数

#include <unistd.h>

int close(int fd);
功能:
    关闭已打开的文件
参数:
    fd : 文件描述符,open()的返回值
返回值:
    成功:0
    失败: -1, 并设置errno

需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。

但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。

16.3 write函数

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
功能:
    把指定数目的数据写到文件(fd)
参数:
    fd :  文件描述符
    buf : 数据首地址
    count : 写入数据的长度(字节)
返回值:
    成功:实际写入数据的字节个数
    失败: - 1

示例:

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

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>


//写文件
int main()
{
    int fd = -1;
    int ret = -1;

    char *str = "hello choice";

    //1.以只写的方式打开一个文件
    fd=open("txt",O_WRONLY | O_CREAT, 0644);
    if(fd == -1)
    {
    
        perror("open");
        return 1;

    }
    printf("fd = %d\n",fd);

    //2.写文件
    ret=write(fd,str,strlen(str));
    if(ret == -1)
    {
    
        perror("write");
        return 1;
    }
    printf("write len = %d\n",ret);

    //3.关闭文件
    close(fd);

    return 0;
}

image-20220922121430828

16.4 read函数

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
功能:
    把指定数目的数据读到内存(缓冲区)
参数:
    fd : 文件描述符
    buf : 内存首地址
    count : 读取的字节个数
返回值:
    成功:实际读取到的字节个数
    失败: - 1

示例:image-20220922123257463

image-20220922123339815

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SIZE 128

int main(void)
{
    int fd = -1;
    int ret = -1;
    char buf[SIZE];

    //1. 打开文件
    fd = open("passwd", O_RDONLY);    
    if (-1 == fd)
    {
        perror("open"); 
        goto err0;
    }

    printf("打开文件ok... fd: %d\n", fd);

    //2. 循环读取数据
    while(1)
    {
        memset(buf, 0, SIZE);
        //每一次从文件中读取最多SIZE个字节
        ret = read(fd, buf, SIZE - 1); 
        if (ret < 0)
        {
            perror("read");
            break; 
        }
    
        printf("%s", buf);
    
        //读到文件结尾
        if (ret < SIZE - 1)
        {
            break; 
        }
    }

    //3. 关闭文件
    close(fd);

    return 0;
err0:
    return 1;
}

阻塞和非阻塞的概念

读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。

从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。

同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

【注意】阻塞与非阻塞是对于文件而言的,而不是指read、write等的属性。

以非阻塞方式打开文件程序示例:

#include <unistd.h> //read
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h> //EAGAIN

int main()
{
    // /dev/tty --> 当前终端设备
    // 以不阻塞方式(O_NONBLOCK)打开终端设备
    int fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);

    char buf[10];
    int n;
    n = read(fd, buf, sizeof(buf));
    if (n < 0)
    {
        // 如果为非阻塞,但是没有数据可读,此时全局变量 errno 被设置为 EAGAIN
        if (errno != EAGAIN)
        {
            perror("read /dev/tty");
            return -1;
        }
        printf("没有数据\n");
    }

    return 0;
}

16.5 lseek函数

#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
功能:
    改变文件的偏移量
参数:
    fd:文件描述符
    offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸。

    whence:其取值如下:
        SEEK_SET:从文件开头移动offset个字节
        SEEK_CUR:从当前位置移动offset个字节
        SEEK_END:从文件末尾移动offset个字节
返回值:c
    若lseek成功执行, 则返回新的偏移量
    如果失败, 返回-1

所有打开的文件都有一个当前文件偏移量(current file offset),以下简称为 cfo。cfo 通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数。

读写操作通常开始于 cfo,并且使 cfo 增大,增量为读写的字节数。文件被打开时,cfo 会被初始化为 0,除非使用了 O_APPEND 。

示例:

#include<stdio.h>
#include<sys/types.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>


#define SIZE 128
int main()
{

    int fd = -1;
    int ret = -1;
    char buf[SIZE];

     //1.读文件

    fd = open("txt",O_RDWR | O_CREAT | O_TRUNC ,0644);
    if(-1 == fd)
    {
        perror("open");
        return 1;
    }
    printf("fd = %d\n",fd);


    //2.lessk函数
    write(fd,"ABCDEFG",7);
    //从文件开头偏移32个字节
    ret = lseek(fd,32,SEEK_SET);
    if(-1 == ret)
    {
    
        perror("lessk");
        return 1;
    }
    write(fd,"1234567890",10);
    memset(buf,0,SIZE);
   ret =  read(fd,buf,SIZE);
   printf("read ret:%d buf: %s\n",ret,buf);

    //3.关闭文件
    
     close(fd);

    return 0;
}

txt文本内容:

image-20220922160956355

“@”在ASCII表示0为’\0’

运行结果:

image-20220922160824005

为什么读取数据为空?这里的ret也等于0,因为指针已经指向尾部了,这里需要重新将指针指向文件开头

//将文件位置指针指向文件开头
lseek(fd,0,SEEK_SET);

image-20220922161952898

%s遇到\0结束,所以只打印ABCDEFG

17. 扩展-ctags使用

ctags(Generate tag files for source code)是vim下方便代码阅读的工具。尽管ctags也可以支持其它编辑器,但是它正式支持的只有VIM。并且VIM中已经默认安装了Ctags,它可以帮助程序员很容易地浏览源代码。

ctags能够定位什么内容?

1)用#define定义的宏

2)枚举型变量的值

3)函数的定义、原型和声明

4)名字空间(namespace)

5)类型定义(typedefs)

6)变量(包括定义和声明)

7)类(class)、结构(struct)、枚举类型(enum)和联合(union)

8)类、结构和联合中成员变量或函数

第一步: 安装ctags

xcc@machine:~/linux-4.16.12$ sudo apt install exuberant-ctags

第二步:/usr/include中生成tags文件

切换到/usr/include

xcc@machine:/usr/include$ pwd /usr/include

生成ctags文件

xcc@machine:/usr/include的密码:

ls -l tags -rw-r–r-- 1 root root 5271877 5月 31 13:00 tags

xcc@machine:/usr/include$

第二个tags

xcc@machine:/usr/src/linux-headers-4.10.0-28 sudo ctags -Rn .

第三步: 配置vimrc

在~/.vimrc文件中最后一行添加如下内容:

set tags+=/usr/include/tags

set tags+=/usr/src/linux-headers-4.10.0-28/tags

第四步: 生效vimrc

执行如下命令

xcc@machine:~/linux-4.16.12$ source ~/.vimrc

Ctrl + ] 表示跟踪代码

Ctrl + t 表示回去

xcc@machine:~$ vim -t STDIN_FILENO

18. 作业

1) 有两个互不相干的文件a.c和b.c,编写Makefile之后,make生成可执行文件a和b。

xcc@machine:~/share/3rd/homework/2makefile$ ls a.c b.c Makefile

xcc@machine:~/share/3rd/homework/2makefile$ make

xcc@machine:~/share/3rd/homework/2makefile$ ls a a.c a.o b b.c b.o Makefile

xcc@machine:~/share/3rd/homework/2makefile$ ./a

hello a

xcc@machine:~/share/3rd/homework/2makefile$ ./b

hello b

2) 在1makefile中有如下目录, 编写Makefile生成可执行文件test

源文件如下:

1527855879183

要求:生成的目标文件存储在obj目录中, 生成的可执行文件test存储在bin目录中。

执行结果如下:

1527856217244

3)使用read和write实现文件拷贝。

19.答案

1) 有两个互不相干的文件a.c和b.c,编写Makefile之后,make生成可执行文件a和b。image-20220922164128710

#一条规则
all: a b


a:a.o
	gcc $< -o $@

b:b.o
	gcc $< -o $@


#模式规则
%.o:%.c
	gcc -c $< -o $@


.PHONY:clean
clean:
	-rm --f a b *.o

2) 在1makefile中有如下目录, 编写Makefile生成可执行文件test

#获取src目录中所有的.c文件
SRC=$(wildcard ./src/*.c)
#替换成目标文件  obj/*.o
OBJS=$(patsubst ./src/%.c, ./obj/%.o, $(SRC))

#变量定义
CC=gcc
INC=./inc
CFLAGS=-c 
TARGET=./bin/test

$(TARGET):$(OBJS)
	$(CC) $^ -o $@
	@#mv $(TARGET) bin/

#将对应的.c生成对应的.o文件
./obj/%.o:./src/%.c
	@#gcc -c -I./inc $< -o $@
	$(CC) $(CFLAGS) -I$(INC) $< -o $@


#伪目标
.PHONY:clean all
clean:
	$(RM) $(OBJS) $(TARGET)

#辅助调试c
all:
	echo $(SRC)
	echo $(OBJS)
	echo $(RM)

3)使用read和write实现文件拷贝。

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

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SIZE 128

//文件的简单拷贝 不支持二进制
int main(void)
{
    int fdr = -1;
    int fdw = -1;
    int ret = -1;

    char buf[SIZE];

    //1. 以只读的方式打开第一个文件
    fdr = open("passwd", O_RDONLY);
    if (-1 == fdr)
    {
        perror("open"); 
        goto err0;
    }

    //2. 以只写的方式打开第二个文件
    fdw = open("txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (-1 == fdw)
    {
        perror("open"); 
        goto err1;
    }

    //3. 循环拷贝数据
    while(1)
    {
        //从第一个文件中读取数据
        memset(buf, 0, SIZE);
        ret = read(fdr, buf, SIZE); 
        if (ret <= 0)
        {
            perror("read");
            break; 
        }
    
        //将读取的数据写入到第二个文件中
        ret = write(fdw, buf, ret); 
        if (ret <= 0)
        {
            break; 
        }
    }

    //4. 关闭两个文件描述符
    close(fdr);
    close(fdw);

    return 0;
err1:
    close(fdr);
err0:
    return 1;
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。