MacOs下sem_init返错问题及延伸思考
一、MacOs下sem_init返错问题
在复习八股文时,常见的一个问题是“有哪些IPC机制?”,详细的答案可参考我个人整理的八股文材料:操作系统线程及进程知识。该材料上的答案是使用ChatGpt生成的,同时我还让ChatGpt生成了各种IPC方式的示例代码。下面是信号量
这种方式的示例代码:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
int count = 0;
sem_t sem;
void *thread_func(void *arg) {
int i;
for (i = 0; i < 1000000; i++) {
sem_wait(&sem);
count++;
sem_post(&sem);
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
sem_init(&sem, 0, 1);
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("count = %d\n", count);
return 0;
}
这段代码在MacOs下的编译过程会产生告警,如下:
sem.c:20:5: warning: 'sem_init' is deprecated [-Wdeprecated-declarations]
sem_init(&sem, 0, 2);
^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/semaphore.h:55:42: note: 'sem_init' has been explicitly marked deprecated here
int sem_init(sem_t *, int, unsigned int) __deprecated;
^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h:211:40: note: expanded from macro '__deprecated'
#define __deprecated __attribute__((__deprecated__))
^
1 warning generated.
出现 deprecated
是说明sem_init
这个函数不建议使用。但在以往的开发经验中,不建议使用并不代表不可以使用。Library提供新函数而要废弃原函数但又不得不保证向下兼容时,也会置函数为deprecated
。因此这个告警并没有引起我的重视。然而在MacOs下运行该程序时,得到的结果不是count=2000000
,count
值是一个介于1990000
到2000000
之间的数。不过在树莓派上运行该程序时,count
值是正常的,即为2000000
。
出现count
值与预期不符,按逻辑推理应该有两种情况:
sem_wait
是非原子操作的函数sem_wait
或sem_init
失败返错了。
根据查询的资料:7.4. Semaphores,sem_wait
是一个原子操作。那么问题应该就出在sem_wait
或sem_init
的返回值上。在代码上对这两个函数的返回值进行处理,如下:
// modify sem_init
int ret = sem_init(&sem, 0, 1);
if (ret != 0) {
perror("sem_init failed");
return ret;
}
// modify sem_wait
int ret = sem_wait(&sem);
if (ret != 0) {
perror("sem_wait failed");
}
修改后执行程序,程序报错如下:
% ./a.out
sem_init failed: Function not implemented
就如同报错所示,sem_init
这个函数其实根本没有实现。也即是说,前面编译时产生的告警可能并不能忽略。那么sem_init
如果不建议使用,应该使用哪个函数初始化信号量呢?
在前面告警中,我们可以获取到信号量相关头文件的路径:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/semaphore.h
。在该文件中,我们可以发现sem_init
的声明下面有个sem_open
的声明,如下:
int sem_init(sem_t *, int, unsigned int) __deprecated;
sem_t * sem_open(const char *, int, ...);
为了确认sem_open
是否是初始化信号量的函数,可以使用man sem_open
命令进行查询,根据手册结果,sem_open
确实用于初始化信号量,如下:
我们将sem_init
替换为sem_open
,修改后的代码如下:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
int count = 0;
sem_t* sem; // sem_t 修改为sem_t*
void *thread_func(void *arg) {
int i;
for (i = 0; i < 1000000; i++) {
int ret = sem_wait(sem); // &sem 修改为 sem
if (ret != 0) {
perror("sem_wait failed");
}
count++;
sem_post(sem); // &sem 修改为 sem
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 修改sem_init的使用为 sem_open,错误处理部分也要相应修改
sem = sem_open("sem_test", O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open failed");
return -1;
}
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("count = %d\n", count);
return 0;
}
修改完程序后重新运行,count
值是正常的,即为2000000
。
二、引发的思考
这个问题并不复杂,也不难定位。但却可以给我们的日常开发带来一定启发:
1. 编译时不要忽略deprecated
告警
根据gcc手册,在gcc编译时,-Wdeprecated-declarations
是默认打开的:
然而我们在出现编译告警时常常会直接忽略,如果在百度上直接搜索-Wdeprecated-declarations
,还会出现很多文章,直接让我们使用-Wno-deprecated-declarations
选项忽略deprecated
告警。
但这种方式并不可取,deprecated
告警有可能并没有向下兼容,而是仅能保证编译通过。最好的方式是:打开-Werror
选项将编译过程中出现的warning
全部作为error
并逐个解决 ,如下:
gcc -Werror sem.c
sem_back.c:20:5: error: 'sem_init' is deprecated [-Werror,-Wdeprecated-declarations]
sem_init(&sem, 0, 1);
^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/semaphore.h:55:42: note: 'sem_init' has been explicitly marked deprecated here
int sem_init(sem_t *, int, unsigned int) __deprecated;
^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h:211:40: note: expanded from macro '__deprecated'
#define __deprecated __attribute__((__deprecated__))
^
1 error generated.
2. 不要忽略对函数返回值的处理
在实际编码时,我们经常会忽略处理函数返回值,但若碰到类似sem_init
的情况而且被弃用的API并没有置为deprecated
,那么开发人员可能需要花费很多时间去调试。
在华为,就有非常严格的规范要求对有返回值的函数均要进行处理
。笔者以前并不理解这一点,认为会使整个代码体量过于庞大。但现在比较认可这一做法,因为相比于未来调试所产生的巨大调试成本,增加一个返回值处理逻辑其实可以降低调试成本。
本文的例子的体量并不大,尚且需要点时间去调试,如果是非常复杂的代码工程而且问题并不能够稳定复现,那么调试成本会极高。如果认为增加的代码逻辑可能会引起性能问题,那么也可以忽略,理由有二:
- 按二八原则,系统中80%的性能问题是由20%的代码产生的,针对性优化即可。
- 针对有性能的代码,可改为使用断言并通过单元测试进行反复测试。
当然增加返回值处理逻辑会使代码体量变大,针对Flash空间比较紧缺的嵌入式设备来说,可直接考虑使用断言,并在发布版本中去掉断言的使用。