title: 应用编程概念与开发环境 tags: [嵌入式Linux, Linux应用编程, 系统调用, 库函数, glibc, gcc, 交叉编译, IMX6ULL] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[01-Linux应用编程基础/02-文件IO基础与深入探究]]、[[01-Linux应用编程基础/03-标准IO库]]、[[01-Linux应用编程基础/04-文件属性与目录]];延伸阅读:[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/01-字符设备驱动基础]]
本篇是应用编程的"地图"。它不教你写具体功能,而是先回答三个绕不开的问题:应用编程到底在和谁打交道(内核)、我们调用的是什么东西(系统调用 / 库函数)、程序从哪里开始、又在哪里结束(main / 返回值 / errno)。最后落地到开发环境:在 Ubuntu 上用 gcc 编译,在 I.MX6ULL 上用交叉编译器编译。
在嵌入式 Linux 平台上做开发,代码大致落在三层上:
| 形态 | 运行位置 | 运行环境 | 典型载体 | 典型产物 |
|---|---|---|---|---|
| 裸机编程 | 直接跑在硬件上 | 无操作系统 | 51、STM32 单片机 | .hex / .bin / .elf |
| 驱动编程 | 内核空间(内核态) | Linux 内核 | 字符设备、平台设备驱动 | .ko 内核模块 |
| 应用编程 | 用户空间(用户态) | Linux 应用层 | open/read/write 等系统调用 | 可执行文件(ELF) |
三者的根本区别在于硬件操作与用户逻辑是否隔离、有没有操作系统做中介。
裸机程序:硬件操作和用户逻辑写在同一份代码里,没有操作系统,编译后直接"裸跑"。
/* LED 裸机程序:硬件操作与用户逻辑混在一起 */
static void led_on(void)
{
/* 点亮 LED 硬件操作代码:直接写寄存器 */
}
static void led_off(void)
{
/* 熄灭 LED 硬件操作代码:直接写寄存器 */
}
int main(void)
{
for ( ; ; ) {
led_on(); /* 点亮 LED */
delay(); /* 延时 */
led_off(); /* 熄灭 LED */
delay(); /* 延时 */
}
}
驱动程序:只实现硬件操作,注册到内核;应用层通过系统调用"遥控"它。下面是一个简化的字符设备 LED 驱动骨架。
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
static void led_on(void)
{
/* 点亮 LED 硬件操作代码 */
}
static void led_off(void)
{
/* 熄灭 LED 硬件操作代码 */
}
static int led_open(struct inode *inode, struct file *filp)
{
/* 打开设备时需要做的事情 */
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t size, loff_t *offt)
{
int flag;
/* 获取应用层 write 的数据,存放在 flag 变量 */
if (copy_from_user(&flag, buf, size))
return -EFAULT;
/* 0 熄灭 LED,非 0 点亮 LED */
if (flag)
led_on();
else
led_off();
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
/* 关闭设备时需要做的事情 */
return 0;
}
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
.release = led_release,
};
static int led_probe(struct platform_device *pdev)
{
/* 驱动加载时需要做的事情 */
return 0;
}
static int led_remove(struct platform_device *pdev)
{
/* 驱动卸载时需要做的事情 */
return 0;
}
static const struct of_device_id led_of_match[] = {
{ .compatible = "alientek,led", },
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, led_of_match);
static struct platform_driver led_driver = {
.driver = {
.owner = THIS_MODULE,
.name = "led",
.of_match_table = led_of_match,
},
.probe = led_probe,
.remove = led_remove,
};
module_platform_driver(led_driver);
MODULE_DESCRIPTION("LED Driver");
MODULE_LICENSE("GPL");
驱动里的 led_fops 把 open / write / release 三个方法注册给内核:应用层调用 open 会执行 led_open,调用 close 会执行 led_release,调用 write 会执行 led_write。约定是写入非 0 点亮、写入 0 熄灭。
应用程序:只写业务逻辑,通过系统调用控制设备,完全不碰寄存器。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd;
int data;
/* 打开 LED 设备(假定设备文件为 /dev/led) */
fd = open("/dev/led", O_WRONLY);
if (0 > fd)
return -1;
for ( ; ; ) {
data = 1;
write(fd, &data, sizeof(data)); /* 写 1 点亮 LED */
sleep(1); /* 延时 1 秒 */
data = 0;
write(fd, &data, sizeof(data)); /* 写 0 熄灭 LED */
sleep(1); /* 延时 1 秒 */
}
close(fd);
return 0;
}
三者关系可以用一张图概括:
flowchart TB
subgraph 用户态["用户态(User Space)"]
A["应用程序<br/>open / read / write / close"]
L["C 标准库 glibc<br/>fopen / fread / printf"]
end
subgraph 内核态["内核态(Kernel Space)"]
S["系统调用接口<br/>sys_open / sys_read ..."]
D["设备驱动<br/>file_operations"]
K["内核核心<br/>进程 / 内存 / VFS"]
end
HW["硬件<br/>LED / GPIO / 磁盘 / 网卡"]
A -->|"系统调用陷入"| S
L -->|"内部再调用系统调用"| S
S --> D
S --> K
D --> HW
K --> HW
应用与驱动是分离的:单独编译、单独部署,一个跑在用户态、一个跑在内核态。这就是应用编程与裸机编程"质的区别"。
系统调用是 Linux 内核提供给应用层的应用编程接口(API),是应用层进入内核的入口。不只是 Linux,所有操作系统都会向应用层提供系统调用。
应用程序通过系统调用请求内核"以自己的名义"执行某些事情:打开磁盘文件、读写文件、关闭文件、控制各种硬件外设。应用层与内核的交互路径如下:
flowchart LR
APP["应用程序"] -->|"调用系统调用 API"| SC["系统调用"]
SC -->|"请求内核服务"| KER["Linux 内核"]
KER -->|"操作"| RES["资源:文件 / 设备 / 内存 / 网络"]
几个关键点:
open、read、write、close、lseek、fork、ioctl 等。库函数是 C 语言库提供的应用层函数库。在 Linux 下通常以动态库(.so)形式提供,存放在根文件系统的 /lib 目录下。
库函数与系统调用的关系要分两类看:
strlen()、strcat()、memcpy()、memset()、strchr() 等,纯用户空间计算;fopen() 内部调用 open()、fread() 内部调用 read()、fwrite() 内部调用 write()。既然可以直接用系统调用,为什么还要设计库函数?因为有些系统调用用起来并不方便,C 语言库函数提供了更方便、更好用、且更具可移植性的调用接口。
| 对比项 | 系统调用 | 库函数(C 库) |
|---|---|---|
| 归属 | 内核提供给应用层的接口,属于内核一部分 | 属于应用层 |
| 运行空间 | 调用时从用户态陷入内核态 | 运行在用户空间 |
| 缓存 | 无缓存 | 通常有缓存(stdio 缓冲),性能、效率通常更优 |
| 可移植性 | 不同操作系统差异大(定义/功能/参数/返回值往往不同) | 不同操作系统接口定义几乎一样,可移植性更好 |
| 例子 | open、read、write、close |
fopen、fread、fwrite、fclose、printf |
flowchart TB
subgraph 用户空间
U1["strlen / memcpy(不涉及系统调用)"]
U2["fopen / fread / fwrite(有 stdio 缓冲)"]
end
subgraph 内核空间
S1["open / read / write ..."]
end
U2 -->|"内部封装"| S1
U1 -.->|"纯用户态计算,无陷入"| U1
从实现者角度看,系统调用与库函数有根本区别;但从使用者角度看,区别并不重要,它们都是 C 语言函数。实际编程中两者都会用到,只需知道"我调用的这个函数是系统调用还是库函数"即可。
一句话总结应用编程:开发 Linux 应用程序,通过调用内核提供的系统调用,或使用 C 库函数,来实现具有相应功能的应用程序。
Linux 下的标准 C 语言函数库是 GNU C 语言函数库(glibc),官方地址:http://www.gnu.org/software/libc/。
C 语言库以动态库文件形式提供,通常存放在 /lib 目录,命名方式通常是 libc.so.6。注意它是一个软链接文件,会链接到真正的库文件。
Ubuntu 16.04 的库文件实际位于 /lib/x86_64-linux-gnu:
ls -l /lib/x86_64-linux-gnu/libc.so.6
# lrwxrwxrwx ... libc.so.6 -> libc-2.23.so
其中 2.23 就是 glibc 的版本号。也可以直接运行共享库获取信息:
/lib/x86_64-linux-gnu/libc.so.6
# GNU C Library ... stable release version 2.23 ...
glibc 源码可直接从 git 仓库下载,也可以通过 ftp 下载。想研究某个库函数的具体实现时,获取源码分析即可。
在 Linux 应用程序中,main 函数是应用程序的入口函数。它有两种常见写法。
int main(void)
{
/* 代码 */
return 0;
}
int main(int argc, char **argv)
{
/* 代码 */
return 0;
}
argc:表示传入参数的个数,包括应用程序自身路径和程序名;argv:字符串指针数组,保存每一个参数。例如运行当前目录下的 hello 并传入参数:
./hello 112233
此时参数个数为 2,且都以字符串形式传递:
| 下标 | 值 | 含义 |
| ---- | -- | ---- |
| argv[0] | "./hello" | 程序自身路径与名字 |
| argv[1] | "112233" | 用户传入的第 1 个参数 |
| argv[2] | NULL | 数组结尾哨兵 |
完整示例:
#include <stdio.h>
int main(int argc, char **argv)
{
int i;
printf("argc = %d\n", argc);
for (i = 0; i < argc; i++)
printf("argv[%d] = %s\n", i, argv[i]);
return 0;
}
gcc -o hello hello.c
./hello 112233 445566
# argc = 3
# argv[0] = ./hello
# argv[1] = 112233
# argv[2] = 445566
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
main还有第三种标准写法,用于接收环境变量:> int main(int argc, char **argv, char **envp); > ``` > > `envp` 是一个字符串数组,每个元素形如 `"NAME=value"`,以 `NULL` 结尾;它不是 `main` 的形式参数,而是紧跟在 `argv` 数组之后由内核放置的。遍历方法: > > ```c > #include <stdio.h> > > int main(int argc, char **argv, char **envp) > { > int i = 0; > while (envp[i] != NULL) { > printf("%s\n", envp[i]); > i++; > } > return 0; > } > ``` > > 也可以直接使用全局变量 `environ` 或 `getenv("PATH")` 获取环境变量。在应用层最常用的是 `getenv()`,`envp` 主要用于需要整体遍历环境的场景。 --- ## 6. 返回值与 errno(初步) Linux 下几乎所有 API 函数都有返回值,用来告诉调用者"执行成功还是失败"。约定规律: - 绝大多数函数**成功返回 0,失败返回负数(通常是 -1)**; - 但并非绝对,例如 `open` 成功返回文件描述符(非负整数)、`read`/`write` 成功返回实际读写字节数、`malloc` 失败返回 `NULL`。 因此常见的判断方式是:c if (func()) {
/* 执行失败 */} else {
/* 执行成功 */}
当我们只知道"失败返回 -1",却不知道失败原因时,就需要 `errno`: - `errno` 本质是一个 `int` 类型的**全局变量**,每个进程维护自己的 `errno`; - 错误发生时,操作系统把错误编号赋给 `errno`;**下一次错误会覆盖上一次**,所以要及时读取; - 并非所有函数出错都会设置 `errno`,这要查 man 手册的返回值描述; - 使用需包含头文件 `<errno.h>`。 把编号翻译成人话,用 `strerror()` 或 `perror()`:c #include #include #include
int main(void) {
printf("errno = %d\n", errno); /* 直接读编号 */ printf("msg = %s\n", strerror(errno)); /* 转成字符串 */ return 0;}
`errno`、`strerror`、`perror` 的完整用法(含错误码表)见 [[01-Linux应用编程基础/02-文件IO基础与深入探究]]。 --- ## 7. 开发环境:Ubuntu + gcc 本书的示例代码围绕 Ubuntu 编写,推荐 16.04 或 14.04(较稳定),也可用 CentOS、Redhat 等发行版。IDE 可选 Eclipse、VSCode;本书使用 **VSCode + gcc**,与正点原子驱动开发文档保持一致。用什么 IDE 都不重要,哪怕直接用 `vi`,重点是学习应用编程本身。 ### 7.1 两个阶段,两套编译器 | 阶段 | 运行目标 | 编译器 | 产物运行位置 | | ---- | -------- | ------ | ------------ | | 入门篇(本模块) | PC 端 Ubuntu | 本地 gcc(x86) | Ubuntu 上直接运行 | | 提高篇(外设) | ALPHA/Mini I.MX6U(ARM 架构) | 交叉编译器 ARM gcc | 开发板上运行 | 入门篇的示例都在 Ubuntu 下测试验证,所以用 `vscode + gcc` 即可;当代码要跑到 I.MX6U 这类 ARM 平台上时,必须用 **交叉编译工具(ARM 架构下的 gcc)** 编译,才能得到能在开发板上运行的可执行文件。 ### 7.2 gcc 本地编译bash gcc -o testApp_1 testApp_1.c
- `gcc`:Ubuntu 下使用的 C 语言编译器; - `-o`:指定生成的可执行文件名; - `testApp_1.c`:待编译的 C 源文件。 ### 7.3 交叉编译bash
以 arm-linux-gnueabihf 工具链为例(I.MX6U 属于 Cortex-A7,带硬件浮点)
arm-linux-gnueabihf-gcc -o testApp_1 testApp_1.c
把可执行文件拷贝到开发板运行
scp testApp_1 root@192.168.1.10:/home/root/
> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 > > **gcc 的编译流程**可分为四步,每一步都可以单独执行、单独观察: > > ```bash > # 1) 预处理:展开宏、头文件,去掉注释 -> .i > gcc -E hello.c -o hello.i > # 2) 编译:把 C 转成汇编 -> .s > gcc -S hello.i -o hello.s > # 3) 汇编:把汇编转成目标文件 -> .o > gcc -c hello.s -o hello.o > # 4) 链接:把目标文件、库链接成可执行文件 > gcc hello.o -o hello > > # 也可一步到位 > gcc hello.c -o hello > ``` > > ```mermaid > flowchart LR > C["hello.c<br/>源文件"] -->|"gcc -E 预处理"| I["hello.i<br/>预处理结果"] > I -->|"gcc -S 编译"| S["hello.s<br/>汇编代码"] > S -->|"gcc -c 汇编"| O["hello.o<br/>目标文件"] > O -->|"gcc 链接(+ libc)"| E["hello<br/>可执行文件"] > ``` > > 常见选项:`-Wall`(开警告)、`-O2`(优化)、`-g`(带调试信息)、`-D宏`(命令行定义宏)、`-I路径`(指定头文件目录)、`-L路径 -l库名`(指定库路径与库)。交叉编译只是把 `gcc` 换成对应的 `arm-...-gcc`,流程完全一致。 ### 7.4 程序运行 编译产物是可在目标平台运行的 ELF 文件。在 Ubuntu 下:bash ./testApp_1
在开发板上:把可执行文件放到文件系统(如通过 `scp`、NFS、SD 卡),修改可执行权限后运行:bash chmod +x testApp_1 ./testApp_1
--- ## 8. 完整示例源码与逐段解释 下面这份程序把本篇的几个要点串起来:`main(argc, argv)` 接收参数、`open/write/close` 系统调用、返回值与 `errno` 判断。它模拟"向 LED 设备写入数据"的应用层逻辑,并打印参数。c #include #include #include #include #include #include #include
int main(int argc, char **argv) {
int fd; int data; int i; /* 1. 打印命令行参数,验证 main 的 argc / argv */ printf("argc = %d\n", argc); for (i = 0; i < argc; i++) printf("argv[%d] = %s\n", i, argv[i]); /* 2. 以只写方式打开设备文件(这里用普通文件代替 /dev/led) */ fd = open("./led_dev", O_WRONLY | O_CREAT, 0664); if (-1 == fd) { /* 3. 出错时用 strerror 把 errno 翻译成可读信息 */ printf("open error: %s\n", strerror(errno)); return -1; } /* 4. 写入非 0 表示点亮 */ data = 1; if (sizeof(data) != write(fd, &data, sizeof(data))) { printf("write error: %s\n", strerror(errno)); close(fd); return -1; } printf("LED on command sent\n"); /* 5. 显式关闭文件,释放文件描述符 */ close(fd); return 0;}
逐段解释: | 行 | 代码 | 说明 | | -- | ---- | ---- | | 1-7 | `#include ...` | `open` 需要 `<sys/types.h>`、`<sys/stat.h>`、`<fcntl.h>`;`write`/`close` 需要 `<unistd.h>`;`printf` 需要 `<stdio.h>`;`errno`/`strerror` 需要 `<errno.h>`、`<string.h>` | | 11 | `int main(int argc, char **argv)` | 需要接收命令行参数,因此用带参写法 | | 17-18 | 参数打印循环 | `argc` 是参数个数,`argv[i]` 是第 i 个字符串 | | 21 | `open(..., O_WRONLY \| O_CREAT, 0664)` | 只写打开,不存在则创建,权限 `rw-rw-r--` | | 22-25 | `if (-1 == fd)` | `open` 失败返回 -1;用 `strerror(errno)` 得到原因 | | 30-34 | `write(fd, &data, sizeof(data))` | 写入 4 字节;返回值是实际写入字节数 | | 39 | `close(fd)` | 不再使用时显式关闭,归还文件描述符 | 编译与运行:bash gcc -Wall -o testApp testApp.c ./testApp hello 123
预期输出:text argc = 3 argv[0] = ./testApp argv[1] = hello argv[2] = 123 LED on command sent
--- ## 9. 实验步骤与调试方法 ### 9.1 实验步骤 1. 在 Ubuntu 下建工作目录并创建源文件:bash mkdir -p ~/vscode_ws/1_chapter cd ~/vscode_ws/1_chapter
2. 用 VSCode 打开该目录,创建 `testApp.c`,粘贴上面的源码。 3. 在 VSCode 菜单"终端 → 新终端"打开终端,编译:bash gcc -Wall -o testApp testApp.c
4. 运行并传入参数:bash ./testApp hello 123
5. 观察 `argc/argv` 输出,以及 `led_dev` 文件是否被创建(`ls -l led_dev`)。 ### 9.2 调试方法 | 现象 | 可能原因 | 排查手段 | | ---- | -------- | -------- | | 编译报 `undefined reference to 'open'` | 漏包含头文件或库未链接 | 检查 `#include <fcntl.h>` 等;确认写法 | | `open error: No such file or directory` | 路径错误或文件不存在且未加 `O_CREAT` | `ls` 确认路径;加 `O_CREAT` | | `open error: Permission denied` | 权限不足 | 检查文件权限 `ls -l`,换路径或提权 | | 参数打印不对 | `argv` 下标越界 | 循环条件用 `i < argc` | | 程序运行无输出 | 可执行文件无执行权限 | `chmod +x testApp` | | 想知道失败原因 | 只看返回值不够 | 加 `perror("open error")` 或 `strerror(errno)` | 调试常用命令:bash gcc -Wall -Wextra -o testApp testApp.c # 打开更多警告 man 2 open # 查看系统调用手册(2 系统调用、3 库函数) echo $? # 查看上一条命令/程序的退出状态 ```
man手册章节编号:
编号 内容 1 Linux 命令 2 系统调用 3 标准 C 库函数
10. 跨平台对比:IMX6ULL vs STM32 vs RK3568
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
维度 I.MX6ULL(本教程) STM32(裸机/RTOS) RK3568 内核 Cortex-A7 Cortex-M3/M4/M7 Cortex-A55 有无 MMU 有,跑完整 Linux 无,通常裸机或 RTOS 有,跑完整 Linux 运行模式 用户态 / 内核态 单一特权模式 用户态 / 内核态 应用编程形态 系统调用 + glibc,ELF 可执行文件 直接寄存器操作 + HAL/LL 库, main永不返回系统调用 + glibc,ELF 可执行文件 交叉工具链 arm-linux-gnueabihf-gccarm-none-eabi-gcc(无 OS 支撑)aarch64-linux-gnu-gcc启动入口 内核加载 ELF/脚本 → _start→main复位向量 → 启动文件 → main内核加载 → _start→main文件系统 有(ext4/UBIFS/rootfs) 无(或用 FatFs 访问 SD 卡) 有 main之外进程、内存、文件、网络全部由内核提供 所有资源自己管理 同 I.MX6ULL 迁移要点 系统调用/库函数接口在 Linux 间基本通用 与 Linux 应用是两套完全不同的编程模型 与 I.MX6ULL 代码几乎一致,注意 64 位与库版本 结论:I.MX6ULL 与 RK3568 同属"Linux 应用编程"范畴,本篇所学的系统调用、glibc、gcc/交叉编译流程可直接复用;STM32 裸机则是另一套世界观,没有系统调用、没有文件描述符、没有
errno,切忌混用概念。
11. 面试精选
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
Q1:系统调用和库函数有什么区别?
答:
- 归属不同:系统调用是内核提供给应用层的接口,属于内核的一部分;库函数属于应用层。
- 运行空间不同:库函数运行在用户空间;调用系统调用会由用户态陷入内核态。
- 缓存不同:库函数通常有缓存(如 stdio 缓冲),系统调用无缓存,因此在性能和效率上库函数通常更优。
- 可移植性不同:不同操作系统的系统调用在定义、功能、参数、返回值上往往不同;而 C 库函数在各系统间接口几乎一致,可移植性更好。
- 联系:部分库函数构建于系统调用之上,如
fopen内部调用open、fread调用read、fwrite调用write;也有库函数(strlen、memcpy)完全不涉及系统调用。- 从使用者角度看,二者都是 C 函数,实际编程中都会用到,知道自己在调用哪一类即可。
Q2:什么是用户态和内核态?应用程序调用
open时发生了什么?答:操作系统下有两种状态:内核态与用户态,应用程序运行在用户态、内核运行在内核态。应用程序调用
open时,会通过系统调用从用户态陷入内核态,内核根据路径找到文件、分配文件描述符并完成打开操作,然后把结果(文件描述符或 -1)返回给用户态。涉及硬件和资源的操作必须由内核在内核态完成,用户态程序不能直接访问。Q3:
main函数的argc、argv各是什么?执行./app a b后它们的值是多少?答:
argc是传入参数个数,包含程序自身路径与程序名;argv是字符串指针数组。执行./app a b后argc == 3;argv[0] == "./app"、argv[1] == "a"、argv[2] == "b",且argv[argc] == NULL。因此遍历时循环条件应写i < argc。Q4:函数返回 -1 就代表失败,但如何知道失败原因?
答:Linux 为常见错误做了编号,函数出错时操作系统会把错误编号写入进程私有的全局变量
errno(int类型,下次出错会被覆盖,要及时读取)。包含<errno.h>即可使用errno;用strerror(errno)可把它转成可读字符串,用perror("...")可直接打印并自动附加冒号和错误描述。需要注意并非所有函数出错都会设置errno,需查阅 man 手册的返回值说明。Q5:入门篇用 gcc,提高篇为什么要用交叉编译?两者产物有何不同?
答:入门篇示例在 PC 端 Ubuntu 上运行验证,CPU 是 x86 架构,用本机 gcc 编译出的 ELF 可直接在 Ubuntu 运行。提高篇的代码要在 ALPHA/Mini I.MX6U 开发板上运行,板子是 ARM 架构,指令集与 x86 不同,必须使用面向 ARM 的交叉编译器(ARM 架构下的 gcc)编译,才能生成在开发板上运行的可执行文件。二者编译流程一致,区别只在于编译器目标架构和最终产物运行的平台。
内容来源:《I.MX6U嵌入式Linux C应用编程指南》第一章 应用编程概念