嵌入式Linux学习笔记(二)

参考文献:【超全面】Linux嵌入式干货学习系列教程_嵌入式教程-CSDN博客

2 应用篇

这一部分参考文献写得非常散,直接以其为索引进行实验式学习。使用wsl2 ubuntu 20.04虚拟机。

2.1 I/O

注意事项:

  1. Windows下,文本流的换行符为\r\n(回车+换行)
  2. 缓冲类型:
    1. 全缓冲:缓冲区满才输出
    2. 行缓冲:遇到换行符输出(stdin/stdout默认)
    3. 无缓冲:直接写入文件(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常用的有:

  1. O_RDONLY:只读打开
  2. O_WRONLY:只写打开
  3. O_RDWR:读写打开
  4. O_TRUNC:清空内容打开
  5. O_APPEND:追加打开
  6. O_CREAT:如果文件不存在,则创建一个
  7. O_EXCL:和O_CREAT配合使用,如果文件存在就报错,不存在则新建一个

使用示例:open("./filename", O_RDWR | O_CREAT);

而mode常用的有:

  1. "r":只读,文件必须存在
  2. "r+":读写,文件必须存在
  3. "w":只写,文件存在则清空,文件不存在则创建
  4. "w+":读写,文件存在则清空,文件不存在则创建
  5. "a":只写,文件存在则追加,文件不存在则创建
  6. "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的情况要更复杂一些。经过实验验证,大概可以得出以下结论:

  1. 根目录下创建/myfifo被禁止,mkfifo直接permission denied。
  2. 在没有读进程已打开的情况下,非阻塞写会直接失败,而阻塞写不会。
  3. 阻塞写存在的风险是:当读进程被Ctrl+C杀掉后,写进程也会立刻退出,并且不会触发sigint_handler,需要额外安装sigpipe来解决“向已经关闭的管道写入数据导致进程终止”的问题(张翔老师的计网实验最有用的一集)。
  4. 总的来说,一读对多写和一写对多读情况都是由“一”这一端创建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);
}

备忘录:

  1. 结合学长的廖老师嵌入式笔记
  2. μCOS源码阅读
  3. FreeRTOS源码阅读
  4. Linux内核源码阅读
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值