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=2000000count值是一个介于19900002000000之间的数。不过在树莓派上运行该程序时,count值是正常的,即为2000000

出现count值与预期不符,按逻辑推理应该有两种情况:

  1. sem_wait是非原子操作的函数
  2. sem_waitsem_init失败返错了。

根据查询的资料:7.4. Semaphoressem_wait是一个原子操作。那么问题应该就出在sem_waitsem_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,那么开发人员可能需要花费很多时间去调试。

在华为,就有非常严格的规范要求对有返回值的函数均要进行处理。笔者以前并不理解这一点,认为会使整个代码体量过于庞大。但现在比较认可这一做法,因为相比于未来调试所产生的巨大调试成本,增加一个返回值处理逻辑其实可以降低调试成本。

本文的例子的体量并不大,尚且需要点时间去调试,如果是非常复杂的代码工程而且问题并不能够稳定复现,那么调试成本会极高。如果认为增加的代码逻辑可能会引起性能问题,那么也可以忽略,理由有二:

  1. 按二八原则,系统中80%的性能问题是由20%的代码产生的,针对性优化即可。
  2. 针对有性能的代码,可改为使用断言并通过单元测试进行反复测试。

当然增加返回值处理逻辑会使代码体量变大,针对Flash空间比较紧缺的嵌入式设备来说,可直接考虑使用断言,并在发布版本中去掉断言的使用。

三、参考资料

  1. Open Computer Systems Fundamentals
  2. gcc手册

留言

您的邮箱地址不会被公开。 必填项已用 * 标注