alsa-lib/src/pcm/pcm_mmap.c
Takashi Iwai 54931e5a54 pcm: Add thread-safety to PCM API
Traditionally, many of ALSA library functions are supposed to be
thread-unsafe, and applications are required to take care of thread
safety by themselves.  However, people never be careful enough, and
almost all applications fail in this regard.

This patch is an attempt to harden the thread safety in exported PCM
functions in a simplistic way: just wrap some of exported functions
with the pthread mutex of each PCM object.  Not all API functions are
wrapped by the mutex since it doesn't make sense.  Instead, the
patchset covers only the functions that may be likely called
concurrently.  The supposedly thread-safe API functions are marked in
the document.

For achieving the feature, two new fields are added snd_pcm_t when the
option is enabled: thread_safe and lock.  The former indicates that
the plugin is thread-safe that doesn't need this workaround and the
latter is the pthread mutex.  Currently only hw plugin have
thread_safe=1.  So, the most of real-time sensitive apps won't be
influenced by this patchset.

Although the patch covers most of PCM ops, a few snd_pcm_fast_ops are
left without the extra mutex locking: namely, the ones that may have
blocking behavior, i.e. resume, drain, readi, writei, readn and
writen.  These are supposed to handle own locking in the callbacks.

Also, if anyone wants to disable this new thread-safe API feature, it
can be still turned off via --disable-thread-safety configure option.

Signed-off-by: Takashi Iwai <tiwai@suse.de>
2016-07-11 15:25:30 +02:00

626 lines
18 KiB
C

/*
* PCM Interface - mmap
* Copyright (c) 2000 by Abramo Bagnara <abramo@alsa-project.org>
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include "config.h"
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <sys/poll.h>
#include <sys/mman.h>
#ifdef HAVE_SYS_SHM_H
#include <sys/shm.h>
#endif
#include "pcm_local.h"
void snd_pcm_mmap_appl_backward(snd_pcm_t *pcm, snd_pcm_uframes_t frames)
{
snd_pcm_sframes_t appl_ptr = *pcm->appl.ptr;
appl_ptr -= frames;
if (appl_ptr < 0)
appl_ptr += pcm->boundary;
*pcm->appl.ptr = appl_ptr;
}
void snd_pcm_mmap_appl_forward(snd_pcm_t *pcm, snd_pcm_uframes_t frames)
{
snd_pcm_uframes_t appl_ptr = *pcm->appl.ptr;
appl_ptr += frames;
if (appl_ptr >= pcm->boundary)
appl_ptr -= pcm->boundary;
*pcm->appl.ptr = appl_ptr;
}
void snd_pcm_mmap_hw_backward(snd_pcm_t *pcm, snd_pcm_uframes_t frames)
{
snd_pcm_sframes_t hw_ptr = *pcm->hw.ptr;
hw_ptr -= frames;
if (hw_ptr < 0)
hw_ptr += pcm->boundary;
*pcm->hw.ptr = hw_ptr;
}
void snd_pcm_mmap_hw_forward(snd_pcm_t *pcm, snd_pcm_uframes_t frames)
{
snd_pcm_uframes_t hw_ptr = *pcm->hw.ptr;
hw_ptr += frames;
if (hw_ptr >= pcm->boundary)
hw_ptr -= pcm->boundary;
*pcm->hw.ptr = hw_ptr;
}
static snd_pcm_sframes_t snd_pcm_mmap_write_areas(snd_pcm_t *pcm,
const snd_pcm_channel_area_t *areas,
snd_pcm_uframes_t offset,
snd_pcm_uframes_t size)
{
snd_pcm_uframes_t xfer = 0;
if (snd_pcm_mmap_playback_avail(pcm) < size) {
SNDMSG("too short avail %ld to size %ld", snd_pcm_mmap_playback_avail(pcm), size);
return -EPIPE;
}
while (size > 0) {
const snd_pcm_channel_area_t *pcm_areas;
snd_pcm_uframes_t pcm_offset;
snd_pcm_uframes_t frames = size;
snd_pcm_sframes_t result;
__snd_pcm_mmap_begin(pcm, &pcm_areas, &pcm_offset, &frames);
snd_pcm_areas_copy(pcm_areas, pcm_offset,
areas, offset,
pcm->channels,
frames, pcm->format);
result = __snd_pcm_mmap_commit(pcm, pcm_offset, frames);
if (result < 0)
return xfer > 0 ? (snd_pcm_sframes_t)xfer : result;
offset += result;
xfer += result;
size -= result;
}
return (snd_pcm_sframes_t)xfer;
}
static snd_pcm_sframes_t snd_pcm_mmap_read_areas(snd_pcm_t *pcm,
const snd_pcm_channel_area_t *areas,
snd_pcm_uframes_t offset,
snd_pcm_uframes_t size)
{
snd_pcm_uframes_t xfer = 0;
if (snd_pcm_mmap_capture_avail(pcm) < size) {
SNDMSG("too short avail %ld to size %ld", snd_pcm_mmap_capture_avail(pcm), size);
return -EPIPE;
}
while (size > 0) {
const snd_pcm_channel_area_t *pcm_areas;
snd_pcm_uframes_t pcm_offset;
snd_pcm_uframes_t frames = size;
snd_pcm_sframes_t result;
__snd_pcm_mmap_begin(pcm, &pcm_areas, &pcm_offset, &frames);
snd_pcm_areas_copy(areas, offset,
pcm_areas, pcm_offset,
pcm->channels,
frames, pcm->format);
result = __snd_pcm_mmap_commit(pcm, pcm_offset, frames);
if (result < 0)
return xfer > 0 ? (snd_pcm_sframes_t)xfer : result;
offset += result;
xfer += result;
size -= result;
}
return (snd_pcm_sframes_t)xfer;
}
/**
* \brief Write interleaved frames to a PCM using direct buffer (mmap)
* \param pcm PCM handle
* \param buffer frames containing buffer
* \param size frames to be written
* \return a positive number of frames actually written otherwise a
* negative error code
* \retval -EBADFD PCM is not in the right state (#SND_PCM_STATE_PREPARED or #SND_PCM_STATE_RUNNING)
* \retval -EPIPE an underrun occurred
* \retval -ESTRPIPE a suspend event occurred (stream is suspended and waiting for an application recovery)
*
* If the blocking behaviour is selected, then routine waits until
* all requested bytes are played or put to the playback ring buffer.
* The count of bytes can be less only if a signal or underrun occurred.
*
* If the non-blocking behaviour is selected, then routine doesn't wait at all.
*/
snd_pcm_sframes_t snd_pcm_mmap_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size)
{
snd_pcm_channel_area_t areas[pcm->channels];
snd_pcm_areas_from_buf(pcm, areas, (void*)buffer);
return snd_pcm_write_areas(pcm, areas, 0, size,
snd_pcm_mmap_write_areas);
}
/**
* \brief Write non interleaved frames to a PCM using direct buffer (mmap)
* \param pcm PCM handle
* \param bufs frames containing buffers (one for each channel)
* \param size frames to be written
* \return a positive number of frames actually written otherwise a
* negative error code
* \retval -EBADFD PCM is not in the right state (#SND_PCM_STATE_PREPARED or #SND_PCM_STATE_RUNNING)
* \retval -EPIPE an underrun occurred
* \retval -ESTRPIPE a suspend event occurred (stream is suspended and waiting for an application recovery)
*
* If the blocking behaviour is selected, then routine waits until
* all requested bytes are played or put to the playback ring buffer.
* The count of bytes can be less only if a signal or underrun occurred.
*
* If the non-blocking behaviour is selected, then routine doesn't wait at all.
*/
snd_pcm_sframes_t snd_pcm_mmap_writen(snd_pcm_t *pcm, void **bufs, snd_pcm_uframes_t size)
{
snd_pcm_channel_area_t areas[pcm->channels];
snd_pcm_areas_from_bufs(pcm, areas, bufs);
return snd_pcm_write_areas(pcm, areas, 0, size,
snd_pcm_mmap_write_areas);
}
/**
* \brief Read interleaved frames from a PCM using direct buffer (mmap)
* \param pcm PCM handle
* \param buffer frames containing buffer
* \param size frames to be written
* \return a positive number of frames actually read otherwise a
* negative error code
* \retval -EBADFD PCM is not in the right state (#SND_PCM_STATE_PREPARED or #SND_PCM_STATE_RUNNING)
* \retval -EPIPE an overrun occurred
* \retval -ESTRPIPE a suspend event occurred (stream is suspended and waiting for an application recovery)
*
* If the blocking behaviour was selected, then routine waits until
* all requested bytes are filled. The count of bytes can be less only
* if a signal or underrun occurred.
*
* If the non-blocking behaviour is selected, then routine doesn't wait at all.
*/
snd_pcm_sframes_t snd_pcm_mmap_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size)
{
snd_pcm_channel_area_t areas[pcm->channels];
snd_pcm_areas_from_buf(pcm, areas, buffer);
return snd_pcm_read_areas(pcm, areas, 0, size,
snd_pcm_mmap_read_areas);
}
/**
* \brief Read non interleaved frames to a PCM using direct buffer (mmap)
* \param pcm PCM handle
* \param bufs frames containing buffers (one for each channel)
* \param size frames to be written
* \return a positive number of frames actually read otherwise a
* negative error code
* \retval -EBADFD PCM is not in the right state (#SND_PCM_STATE_PREPARED or #SND_PCM_STATE_RUNNING)
* \retval -EPIPE an overrun occurred
* \retval -ESTRPIPE a suspend event occurred (stream is suspended and waiting for an application recovery)
*
* If the blocking behaviour was selected, then routine waits until
* all requested bytes are filled. The count of bytes can be less only
* if a signal or underrun occurred.
*
* If the non-blocking behaviour is selected, then routine doesn't wait at all.
*/
snd_pcm_sframes_t snd_pcm_mmap_readn(snd_pcm_t *pcm, void **bufs, snd_pcm_uframes_t size)
{
snd_pcm_channel_area_t areas[pcm->channels];
snd_pcm_areas_from_bufs(pcm, areas, bufs);
return snd_pcm_read_areas(pcm, areas, 0, size,
snd_pcm_mmap_read_areas);
}
int snd_pcm_channel_info_shm(snd_pcm_t *pcm, snd_pcm_channel_info_t *info, int shmid)
{
switch (pcm->access) {
case SND_PCM_ACCESS_MMAP_INTERLEAVED:
case SND_PCM_ACCESS_RW_INTERLEAVED:
info->first = info->channel * pcm->sample_bits;
info->step = pcm->frame_bits;
break;
case SND_PCM_ACCESS_MMAP_NONINTERLEAVED:
case SND_PCM_ACCESS_RW_NONINTERLEAVED:
info->first = 0;
info->step = pcm->sample_bits;
break;
default:
SNDMSG("invalid access type %d", pcm->access);
return -EINVAL;
}
info->addr = 0;
if (pcm->hw_flags & SND_PCM_HW_PARAMS_EXPORT_BUFFER) {
info->type = SND_PCM_AREA_SHM;
info->u.shm.shmid = shmid;
info->u.shm.area = NULL;
} else
info->type = SND_PCM_AREA_LOCAL;
return 0;
}
int snd_pcm_mmap(snd_pcm_t *pcm)
{
int err;
unsigned int c;
assert(pcm);
if (CHECK_SANITY(! pcm->setup)) {
SNDMSG("PCM not set up");
return -EIO;
}
if (CHECK_SANITY(pcm->mmap_channels || pcm->running_areas)) {
SNDMSG("Already mmapped");
return -EBUSY;
}
err = pcm->ops->mmap(pcm);
if (err < 0)
return err;
if (pcm->mmap_shadow)
return 0;
pcm->mmap_channels = calloc(pcm->channels, sizeof(pcm->mmap_channels[0]));
if (!pcm->mmap_channels)
return -ENOMEM;
pcm->running_areas = calloc(pcm->channels, sizeof(pcm->running_areas[0]));
if (!pcm->running_areas) {
free(pcm->mmap_channels);
pcm->mmap_channels = NULL;
return -ENOMEM;
}
for (c = 0; c < pcm->channels; ++c) {
snd_pcm_channel_info_t *i = &pcm->mmap_channels[c];
i->channel = c;
err = snd_pcm_channel_info(pcm, i);
if (err < 0) {
free(pcm->mmap_channels);
free(pcm->running_areas);
pcm->mmap_channels = NULL;
pcm->running_areas = NULL;
return err;
}
}
for (c = 0; c < pcm->channels; ++c) {
snd_pcm_channel_info_t *i = &pcm->mmap_channels[c];
snd_pcm_channel_area_t *a = &pcm->running_areas[c];
char *ptr;
size_t size;
unsigned int c1;
if (i->addr) {
a->addr = i->addr;
a->first = i->first;
a->step = i->step;
continue;
}
size = i->first + i->step * (pcm->buffer_size - 1) + pcm->sample_bits;
for (c1 = c + 1; c1 < pcm->channels; ++c1) {
snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
size_t s;
if (i1->type != i->type)
continue;
switch (i1->type) {
case SND_PCM_AREA_MMAP:
if (i1->u.mmap.fd != i->u.mmap.fd ||
i1->u.mmap.offset != i->u.mmap.offset)
continue;
break;
case SND_PCM_AREA_SHM:
if (i1->u.shm.shmid != i->u.shm.shmid)
continue;
break;
case SND_PCM_AREA_LOCAL:
break;
default:
assert(0);
}
s = i1->first + i1->step * (pcm->buffer_size - 1) + pcm->sample_bits;
if (s > size)
size = s;
}
size = (size + 7) / 8;
size = page_align(size);
switch (i->type) {
case SND_PCM_AREA_MMAP:
ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_FILE|MAP_SHARED, i->u.mmap.fd, i->u.mmap.offset);
if (ptr == MAP_FAILED) {
SYSERR("mmap failed");
return -errno;
}
i->addr = ptr;
break;
case SND_PCM_AREA_SHM:
#ifdef HAVE_SYS_SHM_H
if (i->u.shm.shmid < 0) {
int id;
/* FIXME: safer permission? */
id = shmget(IPC_PRIVATE, size, 0666);
if (id < 0) {
SYSERR("shmget failed");
return -errno;
}
i->u.shm.shmid = id;
ptr = shmat(i->u.shm.shmid, 0, 0);
if (ptr == (void *) -1) {
SYSERR("shmat failed");
return -errno;
}
/* automatically remove segment if not used */
if (shmctl(id, IPC_RMID, NULL) < 0){
SYSERR("shmctl mark remove failed");
return -errno;
}
i->u.shm.area = snd_shm_area_create(id, ptr);
if (i->u.shm.area == NULL) {
SYSERR("snd_shm_area_create failed");
return -ENOMEM;
}
if (pcm->access == SND_PCM_ACCESS_MMAP_INTERLEAVED ||
pcm->access == SND_PCM_ACCESS_RW_INTERLEAVED) {
unsigned int c1;
for (c1 = c + 1; c1 < pcm->channels; c1++) {
snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
if (i1->u.shm.shmid < 0) {
i1->u.shm.shmid = id;
i1->u.shm.area = snd_shm_area_share(i->u.shm.area);
}
}
}
} else {
ptr = shmat(i->u.shm.shmid, 0, 0);
if (ptr == (void*) -1) {
SYSERR("shmat failed");
return -errno;
}
}
i->addr = ptr;
break;
#else
SYSERR("shm support not available");
return -ENOSYS;
#endif
case SND_PCM_AREA_LOCAL:
ptr = malloc(size);
if (ptr == NULL) {
SYSERR("malloc failed");
return -errno;
}
i->addr = ptr;
break;
default:
assert(0);
}
for (c1 = c + 1; c1 < pcm->channels; ++c1) {
snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
if (i1->type != i->type)
continue;
switch (i1->type) {
case SND_PCM_AREA_MMAP:
if (i1->u.mmap.fd != i->u.mmap.fd ||
i1->u.mmap.offset != i->u.mmap.offset)
continue;
break;
case SND_PCM_AREA_SHM:
if (i1->u.shm.shmid != i->u.shm.shmid)
continue;
/* follow thru */
case SND_PCM_AREA_LOCAL:
if (pcm->access != SND_PCM_ACCESS_MMAP_INTERLEAVED &&
pcm->access != SND_PCM_ACCESS_RW_INTERLEAVED)
continue;
break;
default:
assert(0);
}
i1->addr = i->addr;
}
a->addr = i->addr;
a->first = i->first;
a->step = i->step;
}
return 0;
}
int snd_pcm_munmap(snd_pcm_t *pcm)
{
int err;
unsigned int c;
assert(pcm);
if (CHECK_SANITY(! pcm->mmap_channels)) {
SNDMSG("Not mmapped");
return -ENXIO;
}
if (pcm->mmap_shadow)
return pcm->ops->munmap(pcm);
for (c = 0; c < pcm->channels; ++c) {
snd_pcm_channel_info_t *i = &pcm->mmap_channels[c];
unsigned int c1;
size_t size = i->first + i->step * (pcm->buffer_size - 1) + pcm->sample_bits;
if (!i->addr)
continue;
for (c1 = c + 1; c1 < pcm->channels; ++c1) {
snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
size_t s;
if (i1->addr != i->addr)
continue;
i1->addr = NULL;
s = i1->first + i1->step * (pcm->buffer_size - 1) + pcm->sample_bits;
if (s > size)
size = s;
}
size = (size + 7) / 8;
size = page_align(size);
switch (i->type) {
case SND_PCM_AREA_MMAP:
err = munmap(i->addr, size);
if (err < 0) {
SYSERR("mmap failed");
return -errno;
}
errno = 0;
break;
case SND_PCM_AREA_SHM:
#ifdef HAVE_SYS_SHM_H
if (i->u.shm.area) {
snd_shm_area_destroy(i->u.shm.area);
i->u.shm.area = NULL;
if (pcm->access == SND_PCM_ACCESS_MMAP_INTERLEAVED ||
pcm->access == SND_PCM_ACCESS_RW_INTERLEAVED) {
unsigned int c1;
for (c1 = c + 1; c1 < pcm->channels; c1++) {
snd_pcm_channel_info_t *i1 = &pcm->mmap_channels[c1];
if (i1->u.shm.area) {
snd_shm_area_destroy(i1->u.shm.area);
i1->u.shm.area = NULL;
}
}
}
}
break;
#else
SYSERR("shm support not available");
return -ENOSYS;
#endif
case SND_PCM_AREA_LOCAL:
free(i->addr);
break;
default:
assert(0);
}
i->addr = NULL;
}
err = pcm->ops->munmap(pcm);
if (err < 0)
return err;
free(pcm->mmap_channels);
free(pcm->running_areas);
pcm->mmap_channels = NULL;
pcm->running_areas = NULL;
return 0;
}
/* called in pcm lock */
snd_pcm_sframes_t snd_pcm_write_mmap(snd_pcm_t *pcm, snd_pcm_uframes_t offset,
snd_pcm_uframes_t size)
{
snd_pcm_uframes_t xfer = 0;
snd_pcm_sframes_t err = 0;
if (! size)
return 0;
while (xfer < size) {
snd_pcm_uframes_t frames = size - xfer;
snd_pcm_uframes_t cont = pcm->buffer_size - offset;
if (cont < frames)
frames = cont;
switch (pcm->access) {
case SND_PCM_ACCESS_MMAP_INTERLEAVED:
{
const snd_pcm_channel_area_t *a = snd_pcm_mmap_areas(pcm);
const char *buf = snd_pcm_channel_area_addr(a, offset);
snd_pcm_unlock(pcm); /* to avoid deadlock */
err = _snd_pcm_writei(pcm, buf, frames);
snd_pcm_lock(pcm);
if (err >= 0)
frames = err;
break;
}
case SND_PCM_ACCESS_MMAP_NONINTERLEAVED:
{
unsigned int channels = pcm->channels;
unsigned int c;
void *bufs[channels];
const snd_pcm_channel_area_t *areas = snd_pcm_mmap_areas(pcm);
for (c = 0; c < channels; ++c) {
const snd_pcm_channel_area_t *a = &areas[c];
bufs[c] = snd_pcm_channel_area_addr(a, offset);
}
snd_pcm_unlock(pcm); /* to avoid deadlock */
err = _snd_pcm_writen(pcm, bufs, frames);
snd_pcm_lock(pcm);
if (err >= 0)
frames = err;
break;
}
default:
SNDMSG("invalid access type %d", pcm->access);
return -EINVAL;
}
if (err < 0)
break;
xfer += frames;
offset = (offset + frames) % pcm->buffer_size;
}
if (xfer > 0)
return xfer;
return err;
}
/* called in pcm lock */
snd_pcm_sframes_t snd_pcm_read_mmap(snd_pcm_t *pcm, snd_pcm_uframes_t offset,
snd_pcm_uframes_t size)
{
snd_pcm_uframes_t xfer = 0;
snd_pcm_sframes_t err = 0;
if (! size)
return 0;
while (xfer < size) {
snd_pcm_uframes_t frames = size - xfer;
snd_pcm_uframes_t cont = pcm->buffer_size - offset;
if (cont < frames)
frames = cont;
switch (pcm->access) {
case SND_PCM_ACCESS_MMAP_INTERLEAVED:
{
const snd_pcm_channel_area_t *a = snd_pcm_mmap_areas(pcm);
char *buf = snd_pcm_channel_area_addr(a, offset);
snd_pcm_unlock(pcm); /* to avoid deadlock */
err = _snd_pcm_readi(pcm, buf, frames);
snd_pcm_lock(pcm);
if (err >= 0)
frames = err;
break;
}
case SND_PCM_ACCESS_MMAP_NONINTERLEAVED:
{
snd_pcm_uframes_t channels = pcm->channels;
unsigned int c;
void *bufs[channels];
const snd_pcm_channel_area_t *areas = snd_pcm_mmap_areas(pcm);
for (c = 0; c < channels; ++c) {
const snd_pcm_channel_area_t *a = &areas[c];
bufs[c] = snd_pcm_channel_area_addr(a, offset);
}
snd_pcm_unlock(pcm); /* to avoid deadlock */
err = _snd_pcm_readn(pcm->fast_op_arg, bufs, frames);
snd_pcm_lock(pcm);
if (err >= 0)
frames = err;
break;
}
default:
SNDMSG("invalid access type %d", pcm->access);
return -EINVAL;
}
if (err < 0)
break;
xfer += frames;
offset = (offset + frames) % pcm->buffer_size;
}
if (xfer > 0)
return xfer;
return err;
}