[pulseaudio-discuss] Why doesn't mixer control (values) have some kind of locking mechanism? (mutex?)
Takashi Sakamoto
o-takashi at sakamocchi.jp
Thu Aug 6 02:06:01 UTC 2020
Hi,
On Thu, Aug 06, 2020 at 01:31:03AM +0800, Tom Yan wrote:
> Hi all,
>
> I just wonder if it's a "no one cares" or a "no one was aware of it"
> issue (or maybe both?).
>
> When you change (integer) values (e.g. volume) of a mixer control, it
> usually (if not always) involves calling two functions/methods of a
> snd_kcontrol_new, which are get and put, in order to do relative
> volume adjustments. (Apparently it is often done relatively even if we
> have absolute values, for reasons.)
>
> While these two "actions" can be and probably are mostly "atomic"
> (with the help of mutex) in the kernel drivers *respectively*, they
> are not and cannot be atomic as a whole.
>
> This won't really be an issue when the actions (either for one or
> multiple channels) are done "synchronously" in *one* program run (e.g.
> amixer -c STX set Master 1+). However, if such a program run is issued
> multiple times "asynchronously" (e.g. binding it to some
> XF86Audio{Raise,Lower}Volume scroll wheel), volume adjustment becomes
> a total mess / failure.
>
> If it isn't obvious enough. it could happen like the following:
> get1(100 100)
> set1(101 100)
> get2(101 100)
> set2(102 100)
> ...
>
> Or worse:
> get1(100 100)
> get2(100 100)
> set1(101 100)
> set2(100 101)
> ...
>
> Not only that it may/will not finish the first set of adjustments for
> all channels before the second, get() from the second set could happen
> before set() from the first, reverting the effect of the earlier
> one(s).
>
> Certainly one can use something like `flock` with amixer to make sure
> the atomicity of each issue/run, but not only that it looks silly and
> primitive, we don't always manipulate the mixer control with an
> "executable". For example, this weird issue in pulseaudio is probably
> related: https://bugs.freedesktop.org/show_bug.cgi?id=92717
>
> So I wonder, is there a particular reason that mixer control doesn't
> possess some form of lock, which allows any form of userspace
> manipulation to lock it until what should be / is considered atomic is
> finished?
ALSA control core allows applications to lock/unlock a control element
so that any write opreation to the control element fails for processes
except for owner process.
When a process requests `SNDRV_CTL_IOCTL_ELEM_LOCK`[1] against a
control element. After operating the request, the control element is
under 'owned by the process' state. In this state, any request of
`SNDRV_CTL_IOCTL_ELEM_WRITE` from the other processes fails with
`-EPERM`[2]. The write operation from the owner process is successful
only. When the owner process is going to finish, the state is
released[3].
ALSA userspace library, a.k.a alsa-lib, has a pair of
`snd_ctl_elem_lock()` and `snd_ctl_elem_unlock()` as its exported
API[4].
If application developers would like to bring failure to
requests of `SNDRV_CTL_IOCTL_ELEM_WRITE` from the other processes in
the period that the process requests `SNDRV_CTL_IOCTL_ELEM_READ` and
`SNDRV_CTL_IOCTL_ELEM_WRITE` as a transaction, the lock/unlock
mechanism is available. However, as long as I know, it's not used
popularly.
This is a simple demonstration about the above mechanism. PyGObject and
alsa-gobject[5] is required to install:
```
#!/usr/bin/env python3
import gi
gi.require_version('ALSACtl', '0.0')
from gi.repository import ALSACtl
import subprocess
def run_amixer(should_err):
cmd = ('amixer', '-c', str(card_id),
'cset',
'iface={},name="{}",index={},device={},subdevice={},numid={}'.format(
eid.get_iface().value_nick, eid.get_name(),
eid.get_index(), eid.get_device_id(),
eid.get_subdevice_id(), eid.get_numid()),
'0,0',
)
result = subprocess.run(cmd, capture_output=True)
if result.stderr:
err = result.stderr.decode('UTF-8').rstrip()
print(' ', 'expected' if should_err else 'unexpected')
print(' ', err)
if result.stdout:
output = result.stdout.decode('UTF-8').rstrip().split('\n')
print(' ', 'expected' if not should_err else 'unexpected')
print(' ', output[-2])
card_id = 0
card = ALSACtl.Card.new()
card.open(card_id, 0)
for eid in card.get_elem_id_list():
prev_info = card.get_elem_info(eid)
if (prev_info.get_property('type') != ALSACtl.ElemType.INTEGER or
'write' not in prev_info.get_property('access').value_nicks or
'lock' in prev_info.get_property('access').value_nicks):
continue
card.lock_elem(eid, True)
print(' my program locks: "{}"'.format(eid.get_name()))
run_amixer_subprocess(True)
card.lock_elem(eid, False)
print(' my program unlocks: "{}"'.format(eid.get_name()))
run_amixer_subprocess(False)
```
You can see the result of amixer execution is different in the cases of
locked and unlocked, like:
```
$ /tmp/lock-demo
...
my program locks: "Headphone Playback Volume"
expected
amixer: Control hw:1 element write error: Operation not permitted
my program unlocks: "Headphone Playback Volume"
expected
: values=0,0
...
```
[1] https://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound.git/tree/include/uapi/sound/asound.h#n1083
[2] https://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound.git/tree/sound/core/control.c#n1108
[3] https://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound.git/tree/sound/core/control.c#n122
[4] https://www.alsa-project.org/alsa-doc/alsa-lib/group___control.html#ga1fba1f7e08ab11505a617af5d54f4580
[5] https://github.com/alsa-project/alsa-gobject
Regards
Takashi Sakamoto
More information about the pulseaudio-discuss
mailing list