参考文献:【超全面】Linux嵌入式干货学习系列教程_嵌入式教程-CSDN博客
2 应用篇
这一部分参考文献写得非常散,直接以其为索引进行实验式学习。使用wsl2 ubuntu 20.04虚拟机。
2.1 I/O
注意事项:
- Windows下,文本流的换行符为\r\n(回车+换行)
- 缓冲类型:
- 全缓冲:缓冲区满才输出
- 行缓冲:遇到换行符输出(stdin/stdout默认)
- 无缓冲:直接写入文件(stderr默认)
2.1.1 文件的开关
C有两种方法:
int open(const char* path, int oflags); # 系统调用,打开path对应文件,权限模式由oflags给出,成功返回一个文件描述符,失败返回NULL
int close(int fd); # 关闭文件描述符对应的文件,成功返回0,失败返回-1,并改变errno错误码
FILE* fopen(const char* path, const char* mode); # C标准库,打开path对应文件(作为流文件),读写模式由mode给出,成功返回一个FILE指针,失败返回NULL
int fclose(FILE* stream); # 关闭给定的FILE指针对应的流文件,成功返回0,失败返回EOF
其中oflags常用的有:
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR:读写打开
- O_TRUNC:清空内容打开
- O_APPEND:追加打开
- O_CREAT:如果文件不存在,则创建一个
- O_EXCL:和O_CREAT配合使用,如果文件存在就报错,不存在则新建一个
使用示例:open("./filename", O_RDWR | O_CREAT);
而mode常用的有:
- "r":只读,文件必须存在
- "r+":读写,文件必须存在
- "w":只写,文件存在则清空,文件不存在则创建
- "w+":读写,文件存在则清空,文件不存在则创建
- "a":只写,文件存在则追加,文件不存在则创建
- "a+":读写,文件存在则追加,文件不存在则创建
这些字符串后面加上一个b也是一样的。
使用示例:fopen("./filename", "a+");
C++有fstream和filesystem库等方法,此处不赘述。
2.1.2 文件的读写
剩余的部分比较拖沓,我直接放弃参考文献,整理自己在操作系统实验课上学到的东西。
反正C标准库的文件读写就是把每个直接读写的函数名前面加个f,然后把第一个参数变成FILE指针即可。
而如果直接使用系统调用的话,就只有read/write两个操作。
ssize_t read(int fd, void* buf, size_t n); # 从fd表示的文件中读取n个字节装进buf里,返回实际读入的字符数。
ssize_t write(int fd, void* buf, size_t n); # 将buf中的n个字节写入fd表示的文件中,返回实际写入的字符数。
如果需要格式化输出,只需要准备一个数组,然后用sprintf转一下,交给write即可。
int sprintf(char* str, const char* format, ...); # 将格式化字符串放进str中
流定位用lseek,文件属性用lstat,其余在文件系统部分详讲。这一部分都是应用性操作,理解性的东西不多,gpt或者搜索引擎能直接解决。
上述都是Linux系统调用,要实现跨平台兼容的文件操作,看参考文献里的f系列函数操作即可。
2.2 库
2.2.1 静态库
预编译目标文件,链接时直接复制到exe中。
独立可移植,隐蔽而高效,但体积大,更新需要重新编译使用该库的所有程序,且内存占用大(每个进程都会独立加载整个静态库)。
代码:
// libtest.c
#include <stdio.h>
#define TEST4 TEST5
static int test3 = 10;
void test(const char* content) {
printf("hello %s\n", content);
}
int test2;
生成步骤:
gcc -c libtest.c # 生成同名的目标文件
ar -rsv libtest.a libtest.o # 生成的.a文件必须以lib开头,源/目标文件不一定以lib开头,且可以多个.o合并编译成一个静态库
查看符号表:
$ nm libtest.a
# 示例输出
U _GLOBAL_OFFSET_TABLE_
U printf
0000000000000000 T test
0000000000000004 C test2
0000000000000000 d test3
$ nm libtest.a -a # 显示全部符号
# 示例输出
0000000000000000 b .bss # BSS段
0000000000000000 n .comment # 注释段,存储编译器信息
0000000000000000 d .data # 数据段,存储已初始化的全局/静态变量
0000000000000000 r .eh_frame # 异常处理(error_handle)框架的只读段
0000000000000000 n .note.GNU-stack # GNU栈标记,表明栈的特性
0000000000000000 r .note.gnu.property # GNU属性说明,包含编译器信息
0000000000000000 r .rodata # 只读数据段,存储常量
0000000000000000 t .text # 代码段,存储程序实际代码
U _GLOBAL_OFFSET_TABLE_
0000000000000000 a libtest.c
U printf # 未定义符号,表示使用了printf函数
0000000000000000 T test # 定义符号,表示一个函数
0000000000000004 C test2 # 共同符号,表示一个全局变量
0000000000000000 d test3 # 已初始化的全局变量
左侧是地址, 中间是符号类型,后面是符号名。宏定义不会占位置?
类型分为:
- b:BSS段,存放未初始化数据
- n:不可分配符号(注释或标记)
- d:数据段中的符号
- r:只读段中的符号
- t/T:代码段中的符号,其中t是源文件内部使用的,T是可以被其他文件引用的。
- U:未定义符号
- C:共同符号(多个文件共享的全局变量)
静态库的使用:
// test.c
#include <stdio.h>
extern int test2;
int main(void) {
test2 = 114;
char res[5];
sprintf(res, "%d", test2);
test(res);
return 0;
}
gcc test.c -L. -ltest -o test # -L指定库所在的路径,-l指定库的名字(去掉前面的lib)
输出结果:
$ ./test
hello 114
突发奇想nm了一下test:
nm test
0000000000003db8 d _DYNAMIC # ELF中表示动态链接信息的结构
0000000000003fa8 d _GLOBAL_OFFSET_TABLE_ # 全局偏移表
0000000000002000 R _IO_stdin_used # 只读数据段
w _ITM_deregisterTMCloneTable # 注销内存的克隆表,确保事务内存正常
w _ITM_registerTMCloneTable # 注册内存的克隆表
0000000000002184 r __FRAME_END__ # 标识栈帧结束位置
0000000000002014 r __GNU_EH_FRAME_HDR # 标识异常处理(Exception Handling, EH)框架头部信息
0000000000004018 D __TMC_END__ # 标识事务内存克隆表结束位置
0000000000004014 B __bss_start # BSS段起始地址,BSS段存储未初始化的全局变量和静态变量
w __cxa_finalize@@GLIBC_2.2.5 # C++函数,调用全局/静态对象析构函数进行清理
0000000000004000 D __data_start # DATA段起始地址
0000000000001140 t __do_global_dtors_aux # 调用全局/静态对象析构函数
0000000000003db0 d __do_global_dtors_aux_fini_array_entry # 用于全局析构的辅助数组入口
0000000000004008 D __dso_handle # 表示动态共享对象
0000000000003da8 d __frame_dummy_init_array_entry # 初始化栈帧入口
w __gmon_start__ # gprof启动函数,用于性能分析和信息收集
0000000000003db0 d __init_array_end # 全局/静态构造函数数组结束地址
0000000000003da8 d __init_array_start # ...开始地址
00000000000012a0 T __libc_csu_fini # 执行清理操作
0000000000001230 T __libc_csu_init # 初始化操作:设置栈帧,调用构造函数等
U __libc_start_main@@GLIBC_2.2.5 # main入口
U __stack_chk_fail@@GLIBC_2.4 # 检测到栈溢出时调用
0000000000004014 D _edata # 数据段结束地址
0000000000004020 B _end # ELF指示程序结束地址
00000000000012a8 T _fini # 清理操作
0000000000001000 t _init # 初始化操作
00000000000010a0 T _start # 程序入口
0000000000004014 b completed.8061 # ?
0000000000004000 W data_start # 已初始化数据段起始地址
00000000000010d0 t deregister_tm_clones # 注销事务内存中的克隆对象,确保清空相关资源
0000000000001180 t frame_dummy # 初始化栈帧
0000000000001189 T main
U printf@@GLIBC_2.2.5
0000000000001100 t register_tm_clones # 注册事务内存中克隆对象
U sprintf@@GLIBC_2.2.5
00000000000011f8 T test
0000000000004018 B test2
0000000000004010 d test3
大写符号表示全局可见,小写符号表示局部符号或弱符号。
静态库中即使完全没有被用到的符号(test3)也会被搬运到二进制文件中。
各部分执行顺序:
# 初始化顺序
_start
-> _init
-> __libc_csu_init
-> __register_tm_clones
-> __init_array_start ~ __init_array_end
-> main
# 清理顺序
main
-> _fini
-> __libc_csu_fini
-> __do_global_dtors_aux
-> __do_global_dtors_aux_fini_array_entry
-> deregister_tm_clones
2.2.2 动态库
编译时只包含引用信息,运行时动态加载库代码。运行时,OS将动态库映射到进程地址空间。
不用则不加载,且多个进程可以共享同一份动态库代码,减少内存占用。
程序可以自动适应新版本动态库,不需要修改代码。
动态库更新只需替换库文件,不需要重新编译使用该库的所有程序。
// test.c
#include <stdio.h>
extern int test2;
extern void test(const char*); // 犯了个错误,之前没声明
int main(void) {
test2 = 114;
char res[5];
sprintf(res, "%d", test2);
test(res);
return 0;
}
这也显示出静态库和动态库的区别:链接静态库可以不用extern直接引用test。
gcc -shared -fPIC libtest.so libtest.c # 生成动态库
# -shared表示共享(可以被多个程序公用)
# -fPIC表示生成位置无关代码,可以在程序的任意内存地址加载
而nm test的输出变成了:
0000000000003da8 d _DYNAMIC
0000000000003fa8 d _GLOBAL_OFFSET_TABLE_
0000000000002000 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000002154 r __FRAME_END__
0000000000002008 r __GNU_EH_FRAME_HDR
0000000000004010 D __TMC_END__
0000000000004010 B __bss_start
w __cxa_finalize@@GLIBC_2.2.5
0000000000004000 D __data_start
0000000000001140 t __do_global_dtors_aux
0000000000003da0 d __do_global_dtors_aux_fini_array_entry
0000000000004008 D __dso_handle
0000000000003d98 d __frame_dummy_init_array_entry
w __gmon_start__
0000000000003da0 d __init_array_end
0000000000003d98 d __init_array_start
0000000000001270 T __libc_csu_fini
0000000000001200 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
U __stack_chk_fail@@GLIBC_2.4
0000000000004010 D _edata
0000000000004018 B _end
0000000000001278 T _fini
0000000000001000 t _init
00000000000010a0 T _start
0000000000004014 b completed.8061
0000000000004000 W data_start
00000000000010d0 t deregister_tm_clones
0000000000001180 t frame_dummy
0000000000001189 T main
0000000000001100 t register_tm_clones
U sprintf@@GLIBC_2.2.5
U test
0000000000004010 B test2
容易注意到,printf和test3两个符号只出现在链接静态库编译得到的可执行文件中。这是因为动态链接时,编译器只保留对外部符号的引用,而不将动态库代码包含在可执行文件内,编译完成之后得到的可执行文件只保留对外部符号的引用,而动态库中的代码只是在运行时拉到内存里跑。
2.3 进程
2.3.1 基本概念
下图来自参考文献:
- 堆是malloc/free管理的内存区域。
- 栈存放栈帧,包含一次函数调用的返回地址、参数和局部变量,以及上下文切换时保存的寄存器。
- 进程控制块包含PID、进程优先级等信息。
进程分为:
- 交互进程:终端启动,与用户交互。
- 批处理进程:终端无关,被提交到作业队列以顺序执行。
- 守护进程:终端无关,一直在后台运行。
exit是强制退出进程,参数0表示正常,1表示异常。main函数结束后会隐式调用exit。
2.3.2 父子关系
fork()可以产生子进程。它返回两个值,一个给父进程,一个给子进程,通过返回值判断当前是在运行父进程还是子进程。形如
int main() {
pid_t pid = fork();
if(pid == -1) { // fork失败
...
}
if(pid == 0) { // 返回值为0说明是子进程
...
}
else { // 返回值不为0则为子进程进程号,说明是父进程
...
}
}
这种机制感觉有点浪费代码,但其他资源的复用性比较好。
- 子进程继承父进程全部内容,地址空间相互独立。
- 子进程结束时由父进程回收,没被回收的孤儿进程由init进程回收。
- 没被及时回收的成为僵尸进程。
用wait(NULL)和waitpid(-1, NULL, 0)可以最简化地等待进程。有特殊要求可以再查。
2.3.3 exec函数族
进程调用exec函数族执行某个程序,当前内容被指定程序替换,实现父子进程执行不同程序。
#include <unistd.h>
int execl(const char* path, const char* arg, ...); // 指定目标程序的路径
int execlp(const char* file, const char* arg, ...); // 指定目标程序的名称(在PATH中查找)
int execv(const char* path, char* const argv[]); // 把所有参数装进argv里
int execvp(const char* file, char* const argv[]);
arg为目标程序的参数,第0个参数必须要写,但是不会被使用。最后一个必须为NULL。
char* const[]和const char*[]的区别是,前者的指针值不能更改,但指针指向的字符串可以更改,后者的指针值可以更改,但指针值指向的字符串不能被修改。
// exec_test.c
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main(void) {
pid_t pid;
if((pid = fork()) == -1) {
perror("fork failed");
} else if(pid == 0) {
execlp("ls", "ls", "--color", "-l", NULL); // 第0个参数必须要填,最后一个位置必须要是NULL,加上--color确保即使是非交互环境也使用颜色
} else {
execl("/bin/ls", "ls", "-a", "--color", NULL);
wait(NULL);
}
return 0;
}
一个进程只能跑一个exec函数,其后面的都不会被执行,因为整个进程都会被替换掉,原进程不会被继续执行。所以要fork一下才能跑两个exec。
也可以直接跑
#include <stdlib.h>
int system(const char* cmd); // 一个程序可以执行多个命令
2.3.4 守护进程
在后台周期性执行任务或等待事件发生。
简便创建:
nohup xxx & # 创建守护进程
# 其中nohup会忽略挂起信号,使得进程不会因为用户注销而停止
# &是挂到后台
标准创建:
// daemon.c
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
int main(void) {
char str[50];
pid_t pid = fork();
if(pid) exit(0); // 创建子进程,父进程退出
printf("child process running...\n");
// 此时子程序在后台运行
if(setsid() < 0) exit(-1); // 创建新会话,成为新会话组长
printf("session created\n");
chdir("/tmp"); // 更改工作目录,防止因为工作目录卸载而停止
printf("workdir changed\n");
int i = 0;
for(i = 0; i < 3; ++i) close(i); // 关闭标准流文件,脱离终端
int fd = open("/home/divinitist/ws_linux_study/file.txt", O_CREAT | O_RDWR | O_TRUNC);
if(fd == -1) {
perror("open error");
return 0;
}
int cnt = 0;
while(1) {
sprintf(str, "time = %d\n", cnt);
write(fd, (void*)str, strlen(str));
sleep(1);
cnt++;
}
return 0;
}
2.3.5 GDB多进程调试
gdb <二进制文件名称>
# 以下命令在gdb终端下生效
break <函数名> # 在指定函数位置打断点
break <文件名>:<行号> # 在指定行打断点
info breakpoints # 查看断点
delete <断点编号> # 删除指定断点,缺省则删除所有
set follow-fork-mode <child/parent> # 设置跟踪子进程/父进程
set detach-on-fork <on/off> # 跟踪单个/多个进程
info inferiors # 显示跟踪的进程
inferior <跟踪编号> # 切换到指定进程
2.4 线程
线程共享:
- 代码指令
- 静态数据
- 进程文件描述符
- 工作目录
- 用户&用户组ID
线程私有:
- 线程ID
- PC和相关寄存器
- 堆栈
- 错误号
- 优先级
- 执行状态
2.4.1 线程控制
#include <pthread.h>
// 创建线程,其中thread实体需要自己定义,attr按照标准写,routine是线程要执行的函数,arg是对routine函数的传值
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*routine)(void*), void* arg);
// 返回线程id
pthread_t pthread_self(void);
// 结束线程,将最后的返回值传给retval。尽量用这个代替return
void pthread_exit(void* retval);
// 当前线程阻塞直到thread结束,然后接受thread返回值
int pthread_join(pthread_t thread, void** retval);
// 令thread与其主控线程断开关系,不会产生僵尸线程
int pthread_detach(pthread_t thread);
// 线程取消:允许一个线程终止另一个线程的运行,然而需要被取消线程的一些配合
int pthread_cancel(pthread_t thread);
// 设置是否允许线程取消,state为PTHREAD_CANCEL_ENABLE(允许)/PTHREAD_CANCEL_DISABLE(禁止),oldstate用于获取修改前状态,不需要的话填NULL即可
int pthread_setcancelstate(int state, int* oldstate);
// 设置线程取消类型,type为PTHREAD_CANCEL_DEFERRED(等到取消点才取消)/PTHREAD_CANCEL_ASYNCHRONOUS(得到指令立刻取消)
int pthread_setcanceltype(int type, int *oldtype);
// 添加清理非正常结束(包括被取消)的线程资源的函数,routine是要执行的函数,arg是给这个函数传的参
void pthread_cleanup_push(void (*routine)(void*), void* arg);
// 使用由push添加的清理函数,只有在execute不为0的情况下会生效
void pthread_cleanup_pop(int execute);
2.4.2 锁控制
互斥锁:
// 动态方法:初始化一个互斥量,attr用于指定属性,如为NULL则缺省
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 静态方法:直接给一个互斥量赋值一个initializer
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 申请互斥锁
int pthread_mutex_lock(pthread_mutex_t* mutex); // 拿不到锁会一直阻塞
int pthread_mutex_trylock(pthread_mutex_t* mutex); // 拿不到锁会返回EBUSY
// 释放互斥锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
读写锁:
找不到pthread_rwlock_t。这个玩意不是C标准里的东西,而是GNU扩展的。所以设置-std=gnu99或者更高的标准即可编译。intellisense同理修改标准。
与数据库中的概念相同,加读锁会禁止写,但允许同时读(共享);加写锁会禁止读和同时的其他写(排他)。
// gnu99+ standard
#include <pthread.h>
pthread_rwlock_init //初始化一个读写锁
pthread_rwlock_rdlock //读锁定读写锁
pthread_rwlock_tryrdlock //非阻塞读锁定
pthread_rwlock_wrlock //写锁定读写锁
pthread_rwlock_trywrlock //非阻塞写锁定
pthread_rwlock_unlock //解锁读写锁
2.4.3 条件变量
条件变量用于实现线程间协调和通信,允许多个线程在某个条件满足之前等待。
#include <pthread.h>
// 定义条件变量
pthread_cond_t cond;
// 初始化条件变量,第二个参数是attr,一般不用给,直接使用默认
pthread_cond_init(&cond, NULL);
// 释放mutex,挂起线程,等待cond被signal或者broadcast激活,激活后会重新获取mutex,只有成功获取mutex后才会继续执行。
// 一般会while套一个另外的flag判断,避免惊群效应,即多个线程被唤起,但仅需要一个线程来持有资源和解决问题
pthread_cond_wait(&cond, &mutex);
// 通知一个等待线程
pthread_cond_signal(&cond);
// 通知所有等待线程
pthread_cond_broadcast(&cond);
2.4.4 线程池
参考文献自己实现了一个手动线程池,然而GNU Pth有定义好的线程池:
// 需要sudo apt install pth,编译时需要-lpth
#include<pth.h>
pth_init(); // 初始化Pth库
pth_spawn(PTH_ATTR_DEFAULT, thread_func, &thread_id[i]); // 创建新线程
pth_join(thread, NULL); // 等待线程结束
pth_exit(); // 结束当前线程
除了Pth外,还有libuv也可以用一用。
2.4.5 GDB线程调试
info thread # 显示线程
thread <线程号> # 切换线程
break <断点位置> thread <线程号> # 给特定线程打断点
set scheduler-locking on/off # 线程锁,开启时其他线程会暂停,单独调试一个线程
2.5 进程间通信
2.5.1 管道
- 匿名管道pipe:单工,管道大小64K,不能自己读自己写,只能父子/兄弟进程通信
#include <unistd.h> // 给出数组指针,返回两个fd,pfd[0]用来读,pdf[1]用来写。 int pipe(int pfd[2]); // 成功返回0,失败返回-1 // 一般来说是单向用法,即一个进程持有读fd,一个进程持有写fd
- 具名管道fifo:用read/write,不支持lseek定位,单工读写,内容放在内存中,可以非亲缘通信
#include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> // 创建具名管道,path可以给/myfifo这种,mode可以为0666之类。 // 需要包含sys/stat.h和sys/types.h才能使用mkfifo函数,原作者没写 int mkfifo(const char* path, mode_t mode); // gpt说专门用于删除FIFO的函数 int unlink(const char* path); // 用read/write读写,所以用open打开文件描述符,注意不能用O_RDWR打开 open(path, O_RDONLY/O_WRONLY | O_NONBLOCK); // 无阻塞操作 // 如果一个只读描述符都没有,则只写的open会一直阻塞到至少有一个只读open出现 // 每个写进程要么一次写4K个字节,要么一个都不写,保证数据不会交错到一起
实验:匿名管道的父子/兄弟通信,具名管道的通信。
// 匿名管道父子通信
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
int fd[2];
pid_t pid;
if(pipe(fd) == -1) {
perror("pipe");
exit(1);
}
if((pid = fork()) == -1) {
perror("fork");
exit(1);
} else if(pid == 0) {
char msg[] = "hello, parent";
int len = strlen(msg) + 1; // 注意:要把终止符\0也发过去
if(write(fd[1], (void*) msg, len) != len) {
perror("child write");
exit(1);
}
printf("[child] message sent\n");
} else {
char msg[50];
if(read(fd[0], (void*)msg, 50) == -1) {
perror("father read");
exit(1);
}
printf("[parent] msg = \"%s\"\n", msg);
}
return 0;
}
// 类似的兄弟通信
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
int main(void) {
int fd[2];
pid_t pid;
if(pipe(fd) == -1) {
perror("pipe");
exit(1);
}
if((pid = fork()) == -1) {
perror("fork");
exit(1);
} else if(pid == 0) {
char msg[] = "hello, bro";
int len = strlen(msg) + 1; // 注意:要把终止符\0也发过去
if(write(fd[1], (void*) msg, len) != len) {
perror("bro1 write");
exit(1);
}
printf("[bro1] message sent\n");
} else {
pid_t pre_pid = pid;
pid = fork();
if(pid == 0) {
char msg[50];
if(read(fd[0], (void*)msg, 50) == -1) {
perror("bro2 read");
exit(1);
}
printf("[bro2] msg = \"%s\"\n", msg);
} else {
waitpid(pid, NULL, -1);
waitpid(pre_pid, NULL, -1);
}
}
return 0;
}
其实不只是父子/兄弟,更远的亲缘关系也可以,只要在同一份源文件里被定义。
fifo的情况要更复杂一些。经过实验验证,大概可以得出以下结论:
- 根目录下创建/myfifo被禁止,mkfifo直接permission denied。
- 在没有读进程已打开的情况下,非阻塞写会直接失败,而阻塞写不会。
- 阻塞写存在的风险是:当读进程被Ctrl+C杀掉后,写进程也会立刻退出,并且不会触发sigint_handler,需要额外安装sigpipe来解决“向已经关闭的管道写入数据导致进程终止”的问题(张翔老师的计网实验最有用的一集)。
- 总的来说,一读对多写和一写对多读情况都是由“一”这一端创建fifo。两端是否阻塞无所谓,只是对于非阻塞写情况,需要先打开读进程。
写端创建fifo:
// fifo.h
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#define FIFO_PATH "myfifo"
// fifo_sender.c,阻塞写 + sigint + sigpipe
#include "fifo.h"
int sigint;
void sigint_handler(int sig) {
unlink(FIFO_PATH); // 在Ctrl + C关闭时清理现场
printf("myfifo removed\n");
sigint = 1;
exit(0);
}
void sigpipe_handler(int sig) {
unlink(FIFO_PATH); // 在Ctrl + C关闭时清理现场
printf("myfifo removed\n");
sigint = 1;
exit(0);
}
int main(void) {
signal(SIGINT, sigint_handler);
signal(SIGPIPE, sigpipe_handler);
// 不准在根目录下mkfifo,于是在本地目录mkfifo,但是delete删除不掉,只能unlink(函数或命令行)
if(mkfifo(FIFO_PATH, 0777) == -1) {
perror("mkfifo");
exit(1);
}
// 如果没有读进程,非阻塞写进程会打不开fifo
int fd = open(FIFO_PATH, O_WRONLY);
if(fd == -1) {
perror("sender open fifo");
unlink(FIFO_PATH); // 专用删除FIFO
exit(1);
}
char msg[50];
int cnt = 0;
while(!sigint) {
sprintf(msg, "hello stranger (%d)", ++cnt);
int len = strlen(msg) + 1;
if(write(fd, (void*) msg, len) != len) {
perror("sender write");
unlink(FIFO_PATH);
exit(1);
}
sleep(1);
}
return 0;
}
// fifo_receiver.c,阻塞读
#include "fifo.h"
int main(void) {
int fd = open(FIFO_PATH, O_RDONLY);
if(fd == -1) {
perror("receiver open fifo");
exit(1);
}
char msg[50];
while(1) {
ssize_t res = read(fd, (void*) msg, 50);
if(res == -1) {
perror("reader read"); // 如果使用非阻塞读的话,此处会在管道为空或在写时反复报错
continue;
} else if(res) {
printf("[receiver] received \"%s\"\n", msg);
}
}
return 0;
}
读端创建fifo:
// fifo_sender.c
#include "fifo.h"
int main(void) {
// 如果没有读进程,非阻塞写进程会打不开fifo
int fd = open(FIFO_PATH, O_WRONLY);
if(fd == -1) {
perror("sender open fifo");
exit(1);
}
char msg[50];
int cnt = 0;
pid_t pid = getpid();
while(1) {
sprintf(msg, "[%d] hello stranger (%d)", pid, ++cnt);
int len = strlen(msg) + 1;
if(write(fd, (void*) msg, len) != len) {
perror("sender write");
exit(1);
}
sleep(1);
}
return 0;
}
// fifo_receiver.c
#include "fifo.h"
int sigint;
void sigint_handler(int sig) {
unlink(FIFO_PATH); // 在Ctrl + C关闭时清理现场
printf("myfifo removed\n");
sigint = 1;
exit(0);
}
int main(void) {
signal(SIGINT, sigint_handler);
// 不准在根目录下mkfifo,于是在本地目录mkfifo,但是delete删除不掉,只能unlink(函数或命令行)
if(mkfifo(FIFO_PATH, 0777) == -1) {
perror("mkfifo");
exit(1);
}
int fd = open(FIFO_PATH, O_RDONLY);
if(fd == -1) {
perror("receiver open fifo");
unlink(FIFO_PATH); // 专用删除FIFO
exit(1);
}
char msg[50];
while(1) {
ssize_t res = read(fd, (void*) msg, 50);
if(res == -1) {
perror("reader read");
unlink(FIFO_PATH); // 专用删除FIFO
continue;
} else if(res) {
printf("[receiver] received \"%s\"\n", msg);
}
}
return 0;
}
2.5.2 共享内存
把一个文件映射到内存,使得文件I/O在内存中进行,提高效率。
#include <sys/mman.h>
/* addr一般为NULL,即让系统自行分配;length为空间长度;
* prot为权限,类似oflags的组合方式,有PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE,对应读、写、执行和无权限
* flags为映射类型,有MAP_SHARED, MAP_PRIVATE, MAP_ANONYMOUS(匿名,亲缘通信)
* fd为文件描述符,表示要映射的文件
* offset表示文件中映射的起始位置,一般为0(从头开始)
* 成功返回映射区域指针,失败返回MAP_FAILED
*/
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
// 释放映射空间,成功返回0,失败返回-1
int munmap(void* addr, size_t length);
MAP_SHARED表示,对内存中的文件进行的修改都会在释放映射后作用于文件本身;而MAP_PRIVATE表示,这些修改只作用于映射内容,不会改变文件。
来自参考文献:
内存会用若干个整页来装载映射部分的内容,要求分配的页数可能超出所需,访问超出的页(如图3和4)会报总线错误。
共享内存实验:
// mmap_shared_writer.c
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(void) {
// 共享文件需要读写权限:读是为了把文件载入共享内存,写是为了实现修改
int fd = open("share_file", O_RDWR);
if(fd == -1) {
perror("open");
return -1;
}
char *mem = (char *) mmap(NULL, 4096, PROT_WRITE, MAP_SHARED, fd, 0);
if(mem == MAP_FAILED) {
perror("mmap");
return -1;
}
printf("mem = %p\n", mem);
char msg[] = "mmap shared content";
memcpy(mem, msg, strlen(msg) + 1);
if(munmap(mem, 4096) == -1) {
perror("munmap");
return -1;
}
return 0;
}
// mmap_shared_reader.c
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(void) {
int fd = open("share_file", O_RDONLY);
if(fd == -1) {
perror("open");
return -1;
}
char *mem = (char *) mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);
if(mem == MAP_FAILED) {
perror("mmap");
return -1;
}
char msg[50];
memcpy(msg, mem, 50);
int i = 0;
for(i = 0; i < 50; ++i) {
putchar(msg[i]);
}
puts("");
if(munmap(mem, 4096) == -1) {
perror("munmap");
return -1;
}
return 0;
}
2.5.3 System V共享内存
// 指定文件路径和id,为其生成一个key值返回,失败则返回-1
key_t ftok(const char *path, int id);
// 获取共享内存,key确定文件,size为内存大小,shmflg为文件权限 | IPC_CREAT之类的
int shmget(key_t key, int size, int shmflg); // 返回一个shmid,靠shmid可以打开同一个shmat
// 获取共享内存指针,根据shmid确定是哪一个共享内存,shmaddr直接写NULL任由分配,shmflg写0(允许读写)
void *shmat(int shmid, const void *shmaddr, int shmflg); // 只要shmid相同,得到的指针也相同
// 撤销共享内存指针,使指定的shmaddr不可再被访问
int shmdt(void *shmaddr);
// 共享内存管理:一般只考虑删除,即cmd为IPC_RMID的情况,buf填NULL即可(实际上是用来获取/设置共享内存属性的结构体)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
实验:
// sysv_writer.c
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
key_t key = ftok("sysv_file", 114); // id是随意指定的
if(key == -1) {
perror("ftok");
return -1;
}
printf("key = %d\n", key);
int shmid = shmget(key, 512, 0666 | IPC_CREAT);
if(shmid == -1) {
perror("shmget");
return -1;
}
printf("shmid = %d\n", shmid);
// 此处的NULL也是随便系统分配,0表示可读写
void *buf = shmat(shmid, NULL, 0);
strcpy(buf, "system v shm content");
while(1) {sleep(1);}
return 0;
}
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
// 相同路径 + 相同id -> 相同key
key_t key = ftok("sysv_file", 114); // id是随意指定的
if(key == -1) {
perror("ftok");
return -1;
}
printf("key = %d\n", key);
// 相同key + 子集配置(size不大于writer打开的size,权限不高于writer申请的权限) -> 相同shmid
int shmid = shmget(key, 256, 0666 | IPC_CREAT);
if(shmid == -1) {
perror("shmget");
return -1;
}
printf("shmid = %d\n", shmid);
// 此处的0是sysv_writer输出的shmid,因人而异
char *buf = (char *) shmat(shmid, NULL, 0);
printf("read \"%s\"\n", buf);
shmdt(buf);
shmctl(shmid, IPC_RMID, NULL);
}
备忘录:
- 结合学长的廖老师嵌入式笔记
- μCOS源码阅读
- FreeRTOS源码阅读
- Linux内核源码阅读