从0开始学linux韦东山教程第四章问题小结(1)
本人从0开始学习linux,使用的是韦东山的教程,在跟着课程学习的情况下的所遇到的问题的总结,理论虽枯燥但是是基础。说实在的越看视频越感觉他讲的有点乱后续将以他的新版PDF手册为中心,视频作为辅助理解的工具。参考手册为嵌入式Linux应用开发完全手册V5.3_IMX6ULL_Pro开发板。
摘要:这节课手册上没有全部内容,我是根据他的补充视频制作的这个总结博客。这节博客主要讲的是,将hello驱动程序在ubuntu上编译后下载到开发板上,以及makefile文件的优势和语法内容。IO部分,open.c打开文件代码详细解读。
摘要关键词:hello驱动,makefile,open.c
本文详细介绍以下问题,如果你遇到了以下问题,看看我的方案能否解决。
1.上传hello驱动程序
2.什么是makefile,以及部分语法
3.IO部分,open.c打开文件代码详细解读
1.上传hello驱动程序
上传hello驱动程序之前,本人习惯在vc code上先验证一下。如下图所示。
得先确保vs code上安装了gcc以及g++。
gcc --version
g++ --version
gcc -o .\hello hello.c
输入以下命令行,编译测试hello.c文件
小知识,在 VSCode 中,你可以通过设置快捷键来调整字体大小,具体步骤如下:
打开 VSCode。按下 Ctrl + Shift + P(Windows/Linux)或 Cmd + Shift + P(macOS)打开命令面板。输入 Preferences: Open Keyboard Shortcuts (JSON),然后选择该选项,打开 keybindings.json 文件。接下来,你可以添加两个快捷键来增加或减少字体大小。
[{"key": "ctrl+=", // 设置你想要的快捷键,这里是 Ctrl + ="command": "editor.action.fontZoomIn"},{"key": "ctrl+-", // 设置你想要的快捷键,这里是 Ctrl + -"command": "editor.action.fontZoomOut"}
]
2.什么是Makefile
其是一种自动化构建工具的配置文件,通常用于项目开发中,特别是在 C/C++ 等编程语言中。它用于定义和管理项目中各个文件之间的依赖关系,以及如何自动化地编译、链接和构建程序。
编写001_test_app程序
本人编写这方面的程序使用的是VSCode,所以会有些许不同
a.c程序
#include<stdio.h>
extern void func_b();
void main()
{func_b();
}
加上extern void func_b();就不会出现警报了,因为你如果没有告诉a.c这个有这个函数,但是后续找着了,它会觉得突兀。
b.c程序
#include<stdio.h>
void func_b()
{printf("hello world! This is b print.");
}
以上程序构建完成之后输入命令行在下方窗口
gcc -o test a.c b.c
./test
其实你如果细致的话可以看到,我最开始也没有声明extern,导致有所警报。
win11测试过了,到Ubuntu其实肯定也没有问题。演示一遍吧。
首先迁移
我把makefile文件夹放到了ubuntu中的nfs_rootfs下。
我是直接从ubuntu中打开的,首先输入ls查看当先目录文件,然后cd 打开nfs_rootfs文件夹,然后cd打开makefile文件夹,ls查看一下,发现里面还有一个文件,打开001_test_app文件夹,ls再查看一下,把test.exe删除一下。重新输入以上命令行。
ls
cd nfs_rootfs
cd makefile
ls
cd 001_test_app
ls
rm test.exe
gcc -o test a.c b.c
./test
木有问题。
gcc编译缺点:课上讲了这个点,我觉得恒重要,就是gcc编译效率问题,它会将文件都编译一遍,其实需要编译的也就那几个文件。对于庞大的工程来说效率太低了。
视频中建议是分开将其的.o文件编译,然后再统一编译,这样效率高。后续也就引出了makefile的概念。执行make命令时在当前文件路径下查找makefile文件。
具体来说,make 会根据目标文件(在这个例子中是 test)的时间戳和它所依赖的文件(如 a.o 和 b.o)的时间戳来判断是否需要重新构建。以下是发生的可能情况:
test 已经是最新的:如果目标文件 test 的时间戳比它的依赖文件 a.o 和 b.o 都更新,make 会认为 test 已经是最新的,不需要重新编译。
a.o 和 b.o 已经是最新的:如果 a.o 和 b.o 的时间戳比它们各自的源文件(如 a.c 和 b.c)还要新,make 认为这些对象文件已经是最新的,因此也不会重新编译它们。
makefile部分语法
test:a.o b.o c.ogcc -o test $^
%.o:%.cgcc -c -o $@ $<
test: a.o b.o c.o:这里 test 是目标,表示最终要生成的文件(可执行文件)。a.o, b.o, 和 c.o 是 test 的依赖文件(目标文件)
gcc -o test $^
:这条命令使用 gcc 编译器将依赖的目标文件(a.o, b.o, c.o)链接成最终的可执行文件 test。其中,$^
是自动变量,代表所有的依赖文件(即 a.o b.o c.o),用于表示链接时要包括的对象文件。
%.o: %.c:这是一条模式规则,表示所有的 .o 文件是由同名的 .c 文件编译而来的。% 是一个通配符,表示任何文件名。因此,它表示“对于所有 .c 文件,生成相应的 .o 文件”。
gcc -c -o $@ $<:这条命令使用 gcc 编译器将 .c 文件编译成 .o 文件。-c 表示只编译不链接,-o 后面跟着目标文件名。
$@ 是自动变量,代表当前规则中的目标文件,在这里就是 .o 文件。
$< 是自动变量,代表当前规则中的第一个依赖文件,在这里就是对应的 .c 文件
make clean这个命令我之前学过,可以看出他就是执行你想去除的文件。后续语法就不具体讲解了。
IO部分
open.c代码
接下来详细解析一下它的代码,做到知道为什么,知道怎么改,怎么来的。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./open 1.txt* argc = 2* argv[0] = "./open"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}while (1){sleep(10);}close(fd);return 0;
}
man man
man man 这个命令的作用是显示 man 命令本身的帮助页面,也就是查看关于 man 命令的详细文档。
2 System calls (functions provided by the kernel)3 Library calls (functions within program libraries)
2.
System calls (functions provided by the kernel):这一章节包含 系统调用 的文档。系统调用是程序通过它们与操作系统内核进行交互的接口。当程序需要执行操作系统级的任务(比如文件操作、内存分配、进程管理等)时,就会调用系统调用。这些函数由操作系统内核提供。例如,open、read、write 等函数就属于系统调用,它们允许程序与文件系统、I/O 设备等进行交互。
3.
Library calls (functions within program libraries):这一章节包含 库函数 的文档。库函数是程序开发中常用的一些函数,它们不直接由操作系统提供,而是由标准库或其他编程库提供。例如,C 标准库中的 printf、malloc 等函数就是库函数。它们通常在应用程序中被用来简化常见的操作。
小结函数主要也就是通过man 2 函数,man 3 函数来查看
构建open.c文件
就得知道open这个函数需要啥,得了解它,既然是内核函数那就得写以下命令行
man 2 open
如下图所示,open函数,openat函数,creat函数都是int类型,所以返回的肯定是一个值,我得用一个int型变量接受,以及他们要用的头文件我得囊括进去。所以就有了以下代码。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int fd; //负责接受值
printf 函数
我想查看打印资料,得用输出字符串函数。
if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}
printf它肯定不是内核函数所以,我用以下命令行查看。
man 3 printf
如下图所示文件中写了所需要的头文件,以及函数构造啥的。
所以我有个问题,为什么int printf(const char *format, …);是int类型,但是我从来没有填写一个变量去接受值呀,一般不是直接printf(“hello!”)直接输出吗?
答案:printf 函数的返回类型是 int,这是因为它的返回值表示打印的字符数(成功打印的字符数)。虽然在常见的使用中,你通常不会直接捕获这个返回值,但它是 printf 函数设计的一部分。
printf 返回一个 整数,表示成功写入到标准输出(即屏幕)上的字符数。
如果发生错误,printf 会返回一个负值(通常是 -1),表示输出失败。
int num_chars = printf("Hello, world!\n");
printf("Printed %d characters.\n", num_chars);
在这种情况下,你可以捕获返回值 num_chars 来知道打印了多少个字符。这也就是为什么,不填写一个变量接受。
要想打开一个文件,那么输入一定是 ./open 1.txt大于等于两个字符的,所以具体的实现代码如下。
if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}
而argv[1]对应的也就是1.txt,也就是需要我fd去接受的。
fd = open(argv[1], O_RDWR);
argv[1]:表示命令行中传入的第二个参数,即你想要打开的文件名。
O_RDWR:表示以 读写模式(read-write)打开文件。如果文件不存在,或者发生其他错误,open 会返回一个负值(通常是 -1)。
所以当我反馈值小于0时,得输出错误,输出错误的函数得学。
if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}
输入以下命令行查看输出错误函数
man 3 errno
本人将以上3段话翻译了一下,errno 的值只有在系统调用返回错误时(例如,大多数系统调用返回 -1;大多数库函数返回 -1 或 NULL)才有意义;成功的函数调用可以修改 errno。系统调用或库函数从不将 errno 的值设置为零。
对于某些系统调用和库函数(例如,getpriority(2)),-1 是成功的有效返回值。在这种情况下,可以通过在调用前将 errno 设置为零,之后检查返回值是否可能指示发生了错误,并通过检查 errno 是否非零来区分成功返回和错误返回。
errno 根据 ISO C 标准定义为一个可修改的整型左值,不得显式声明;errno 可能是一个宏。errno 是线程局部的;在一个线程中设置它不会影响其他线程中的值。
意思就是,假设我的输入为./open 2.txt这个文件其实不存在。程序会尝试打开 2.txt,但是由于文件不存在,open 调用将失败并返回 -1。errno 的值会被设置为 ENOENT,即错误代码 2,表示“没有这样的文件或目录”。
如图所示,不同值对应的不同错误。
一直翻,翻到文档的最下边,对类似应用的介绍。
这里也就引出了strerror(errno)这个函数。
man 3 strerror
我们用char函数输入错误代码值,
此处详细介绍了这个函数,strerror() 函数返回一个指向字符串的指针,该字符串描述了作为参数传入的错误码 errnum,并且可能使用当前区域设置的 LC_MESSAGES 部分来选择合适的语言。(例如,如果 errnum 是 EINVAL,返回的描述将是“Invalid argument”(无效参数))。这个字符串不能被应用程序修改,但可能会被后续对 strerror() 或 strerror_l() 的调用修改。除了这些函数外,没有其他库函数(包括 perror(3))会修改该字符串。
说人话就是,strerror(2) 会输出No such file or directory,strerror(3) 会输出No such process.对应上图。
man 3 perror
翻译一下以上圈红部分,看看这个函数作用,perror() 函数会在标准错误输出上打印出描述上一次系统或库函数调用时遇到的错误信息。首先(如果 s 不是 NULL 且 *s 不是空字节(‘\0’)),会打印传入的字符串 s,后面跟上冒号和一个空格。然后打印出与当前 errno 值对应的错误信息,并以换行符结尾。为了更好地使用这个函数,传递给它的字符串通常应当包含发生错误的函数名称。
说人话就是,假设我的输入为./open 2.txt这个文件其实不存在,perror 通过访问全局变量 errno 来获得错误码,并根据该错误码来输出相应的错误信息。如果你传递给它的字符串(例如 “open”)不是空的,它就会将这个字符串作为前缀输出,后跟错误描述。
man 3 sleep
sleep调用的头文件,以及函数作用。
sleep() 函数使调用线程进入休眠状态,直到指定的实际秒数(seconds)过去,或者直到一个未被忽略的信号到达。
我直接在我的vs code上测试的,当我2.txt文件不存在时,出现以下报错。