alsa-lib的unable to create IPC shm instance问题的解决

                                                                                           2015-12-08 19:34

### 问题描述:
一个开机自动启动app,该app调用alsa-lib,有一定概率该app启动时,alsa-lib报出如下错误信息:
unable to create IPC shm instance
并导致没有声音之类的问题。

### 调查过程:
1.
下载最新alsa-lib代码并解压,地址如下:
ftp://ftp.alsa-project.org/pub/lib/alsa-lib-1.1.0.tar.bz2
2.
编译alsa-lib并安装部署
3.
重现问题
4.
确定当前环境可以重现问题后,给alsa-lib的代码加上必要的log,然后再编译再安装部署,
并把直接启动app改为使用strace启动app,命令如下:
strace -e trace=ipc,process -f app
5.
再重现问题并保存log
6.
分析log,发现问题出现时,在alsa-lib-1.1.0/src/pcm/pcm_direct.c的110行的shmat()
和117行的shmctl()执行之间,插入了一个clone的执行;当问题不出现时,shmat()和shmctl()
执行之间没有clone的执行。
clone的log如下(数值都改为了X):
[pid  XXXX] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0xXXXXXXXX) = XXXX
从clone的参数来看,可能是执行了fork(),
并且能够通过在alsa-lib中增加log来确认这个clone不是alsa-lib执行的,那么就应该是app执行的。
根据`man shmctl`,shm_nattch为"No. of current attaches",
一个进程执行shmat()后,shm_nattch的值应该为1,之后再执行fork(),
attach到共享内存的进程数就变为2,shm_nattch的值也应该变为2,写个
程序验证下,程序中有如下3个关键步骤:
 1) shmat()
 2) fork()
 3) 检查shm_nattch的值
代码如下:
  1 #include
  2 #include
  3 #include
  4 #include
  5 #include
  6 #include
  7 
  8 int main(int argc, char *argv[])
  9 {
 10    /*容错就先免了啊*/
 11    int r = 0;
 12    struct shmid_ds buf;
 13    key_t ipc_key = (key_t)325559;
 14    size_t shmsize = 488;
 15    int shmid = -1;
 16    mode_t ipc_perm = 0666;
 17    void *shmptr = NULL;
 18    pid_t pid;
 19 
 20    printf("___%d:%d call shmget(%d,%u,%d)\n", __LINE__, getpid(), (int)(ipc_key), shmsize, IPC_CREAT | ipc_perm);
 21    shmid = shmget(ipc_key, shmsize, IPC_CREAT | ipc_perm);
 22    printf("___%d:%d ret shmget=%d\n", __LINE__, getpid(), shmid);
 23 
 24    printf("___%d:%d call shmat(%d,0,0)\n", __LINE__, getpid(), shmid);
 25    shmptr = shmat(shmid, 0, 0);
 26    printf("___%d:%d ret shmat=%p\n", __LINE__, getpid(), shmptr);
 27 
 28    printf("+++%d:%d call fork\n", __LINE__, getpid());
 29    pid = fork();
 30    printf("+++%d:%d ret fork=%d\n", __LINE__, getpid(), (int)pid);
 31 
 32    usleep(100000);
 33 
 34    printf("___%d:%d call shmctl(%d,IPC_STAT,%p)\n", __LINE__, getpid(), shmid, &buf);
 35    r = shmctl(shmid, IPC_STAT, &buf);
 36    printf("___%d:%d ret shmctl=%d\n", __LINE__, getpid(), r);
 37    printf("___%d:%d nattch=%lu\n", __LINE__, getpid(), buf.shm_nattch);
 38 
 39    printf("___%d:%d call shmdt(%p)\n", __LINE__, getpid(), shmptr);
 40    r = shmdt(shmptr);
 41    printf("___%d:%d ret shmdt=%d\n", __LINE__, getpid(), r);
 42 
 43    printf("___%d:%d call shmctl(%d,IPC_RMID,NULL)\n", __LINE__, getpid(), shmid);
 44    r = shmctl(shmid, IPC_RMID, NULL);
 45    printf("___%d:%d ret shmctl=%d\n", __LINE__, getpid(), r);
 46 
 47    if ((pid_t)0 != pid) {
 48       printf("+++%d:%d call wait(NULL)\n", __LINE__, getpid());
 49       pid = wait(NULL);
 50       printf("+++%d:%d ret wait=%d\n", __LINE__, getpid(), (int)pid);
 51    }
 52 
 53    return 0;
 54 }
执行多次后,父进程中shm_nattch的值一直为2,子进程中shm_nattch的值有时为1有时为2。
alsa-lib出现问题的情况,也就是上述程序所示的情况,此时shm_nattch的值为2了,
alsa-lib-1.1.0/src/pcm/pcm_direct.c的122行的条件判断"if (buf.shm_nattch == 1)"
就不成立了,但此时刚创建的共享内存还没被使用过,应该走if成立的分支,但此时却会走else分支,
这就是问题所在了。

### 解决方式:
alsa-lib-1.1.0/src/pcm/pcm_direct.c的128行的代码如下:
 128       dmix->shmptr->magic = SND_PCM_DIRECT_MAGIC;
说明alsa-lib第1次使用共享内存时会把其中的magic赋值为SND_PCM_DIRECT_MAGIC,
并且`man shmget`中有如下一句话:
       When a new shared memory segment is created, its contents are initialized to zero values
上句话说明共享内存刚被创建时,magic的值为0,
因此可以根据共享内存中magic的值来判断共享内存是否被第1次使用过。
所以,把alsa-lib-1.1.0/src/pcm/pcm_direct.c的122行的代码由如下:
 122    if (buf.shm_nattch == 1) { /* we're the first user, clear the segment */
改为如下:
 122    if (dmix->shmptr->magic != SND_PCM_DIRECT_MAGIC) { /* we're the first user, clear the segment */
就解决了问题。
原打算把"buf.shm_nattch == 1"这个条件也保留,即将if语句中的条件改为
"buf.shm_nattch == 1 || dmix->shmptr->magic != SND_PCM_DIRECT_MAGIC",
但考虑到一种情况,即父进程先执行时shm_nattch为2,父进程执行一次if成立的代码块,子进程
后执行时shm_nattch为1,子进程也会执行一次if成立的代码块,而if成立的代码块应该只被执行
一次,所以"buf.shm_nattch == 1"这个条件不能保留。

### 其它思考:
问题出现时导致问题的进程是由app创建的,如果app不创建那个进程,alsa-lib也不会出这个问题,
但从alsa-lib的代码能看到它有信号量这种进程同步机制,说明alsa-lib考虑了多进程同步的事,
但考虑得有漏洞,没考虑到使用共享内存的过程中,在某个节点时出现进程创建的情况,
如果alsa-lib完全不考虑多进程并且声明自己不支持多进程,那么这个问题就是app要考虑的事,
但既然alsa-lib考虑了多进程的事,出现这个问题就应该是alsa-lib的事。

### 更新:
我做了patch,提交到alsa官方后,alsa的人提出了如果系统中有程序使用和alsa-lib一样的ipc key的冲突问题,这是概率很低的事,alsa的那个人很严谨。
后来和alsa的那个人讨论出了一个更健壮的方案,这个方案让着和alsa-lib使用一样的ipc key的程序,如果ipc key冲突了,alsa-lib可以改配置文件来更改ipc key。
方案就是先尝试连接现有的shared mem,如果成功了,说明当前进程不是第1个使用者;如果失败,再尝试以互斥方式创建新shared mem,如果成功了,说明当前进程是第1个使用者。
这个方案也是我平时使用POSIX shared mem的方式,和alsa的那个人讨论时没想着说出来。
我实现方案并测试并做出patch后,alsa的那个人接受了并说是nice improvement。
相关alsa-lib变更记录见:
http://git.alsa-project.org/?p=alsa-lib.git;a=commitdiff;h=dec428c352217010e4b8bd750d302b8062339d32
changelog中从“More notes about this patch”开始的部分是alsa的那个人给添加的。

你可能感兴趣的:(Linux)