Merge ~dirk.zimoch/epics-base:epicsMutexPriorityInheritance into ~epics-core/epics-base/+git/epics-base:7.0

Proposed by Dirk Zimoch
Status: Merged
Merge reported by: mdavidsaver
Merged at revision: 4855d9e082e2191b2517822d8466e6148e5f6994
Proposed branch: ~dirk.zimoch/epics-base:epicsMutexPriorityInheritance
Merge into: ~epics-core/epics-base/+git/epics-base:7.0
Diff against target: 780 lines (+390/-225) (has conflicts)
9 files modified
documentation/RELEASE_NOTES.md (+9/-0)
modules/libcom/src/osi/Makefile (+1/-0)
modules/libcom/src/osi/os/posix/osdEvent.c (+3/-2)
modules/libcom/src/osi/os/posix/osdMutex.c (+73/-217)
modules/libcom/src/osi/os/posix/osdPosixMutexPriv.h (+28/-0)
modules/libcom/src/osi/os/posix/osdSpin.c (+4/-2)
modules/libcom/src/osi/os/posix/osdThread.c (+5/-4)
modules/libcom/test/Makefile (+9/-0)
modules/libcom/test/epicsMutexPriorityInversionTest.c (+258/-0)
Conflict in documentation/RELEASE_NOTES.md
Reviewer Review Type Date Requested Status
mdavidsaver Approve
Dirk Zimoch (community) Approve
Andrew Johnson Needs Fixing
Review via email: mp+394327@code.launchpad.net

Description of the change

This change enables priority inheritance for posix mutexes and thus prevents priority inversion problems on real-time Linux systems.

For backward compatibility reasons, this feature must be explicitly enabled by setting the environment variable EPICS_MUTEX_USE_PRIORITY_INHERITANCE=YES before libCom is loaded.

Is only meaningful when running with SCHED_FIFO scheduling, which requires sufficient priviledges to set. That typically means to run the IOC as root.

To post a comment you must log in.
Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Test does not succeed when not running as root or EPICS_MUTEX_USE_PRIORITY_INHERITANCE is not set before.

review: Needs Fixing
e8a0c17... by Till Straumann

Support run-time enabled priority-inheritance for epicsMutex, epicsEvent etc.

Revision history for this message
Ralph Lange (ralph-lange) wrote :

> ...SCHED_FIFO scheduling, which requires sufficient privileges to set. That typically means to run the IOC as root.

FWIW, IOCs should *not* be run as root.
The EPICS website has detailed instructions at
https://epics-controls.org/resources-and-support/documents/howto-documents/posix-thread-priority/

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

I see in posix/osdMutex.c that

> /* Until these can be demonstrated to work leave them undefined*/
> /* On solaris 8 _POSIX_THREAD_PRIO_INHERIT fails*/
> #undef _POSIX_THREAD_PROCESS_SHARED
> #undef _POSIX_THREAD_PRIO_INHERIT

has been present since 2002. I would naively think that the various sites using RTLinux flavors which have noticed quickly if epicsMutex didn't have priority inheritance.

Can Dirk (or anyone) give some background on why this change is being proposed now?

Also, some significant complexity is being added in the !HAVE_RECURSIVE_MUTEX case. Is this implementation actually used by any currently supported targets?

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

We found that EPICS was not behaving as "real-time" as expected even though SCHED_FIFO was used. A deeper investigation found that this was due to prioriy inversion. When reading the source code, we found that "Until these can be demonstrated to work leave them undefined" has been in for a long time and nobody seemed to have cared, which had surprized us a lot. I think this wither got forgotten somehow and nobody really needed it or noticed it or maybe people simply removed those #undefs (which is not sufficient because other code used posix semaphores directly instead of using epicsMutexes).

I propose this change now, because we have fixed it now.

Till needs to comment on details of the implementation...

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

> this wither got forgotten
* either

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

I tried the simply minded (early morning) test of removing the two #undef lines. It seems like to have the desired effect. pthread_mutexattr_setprotocol(..., PTHREAD_PRIO_INHERIT) succeeds with for a normal unprivleged user. I added some prints to confirm that status==0.

Am I missing something obvious?

I'm testing with Debian 10, where _XOPEN_SOURCE=700 (by way of -D_GNU_SOURCE).

Also, what is the motivation to economize pthread_mutexattr_t? I see __SIZEOF_PTHREAD_MUTEXATTR_T==4 (on a 64-bit host).

Revision history for this message
Till Straumann (tillstraumann) wrote :

> We found that EPICS was not behaving as "real-time" as expected even though
> SCHED_FIFO was used. A deeper investigation found that this was due to prioriy
> inversion. When reading the source code, we found that "Until these can be
> demonstrated to work leave them undefined" has been in for a long time and
> nobody seemed to have cared, which had surprized us a lot. I think this wither
> got forgotten somehow and nobody really needed it or noticed it or maybe
> people simply removed those #undefs (which is not sufficient because other
> code used posix semaphores directly instead of using epicsMutexes).
>
> I propose this change now, because we have fixed it now.
>
> Till needs to comment on details of the implementation...

At SLAC we had -- for years -- patched this old #undef. What I only noticed recently is that even if you patch the #undef EPICS still uses non-PI mutexes in some other places (e.g., the mutex protecting the condition variable in epicsEvent) and this is what this particular contribution addresses.

Re - 'root': it is absolutely correct that you don't have to be root to use SCHED_FIFO (and at SLAC we had followed the guidelines linked by Ralph). Using 'root' is just the easiest way to get there...

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

Another question, what motivates the addition of epicsPosixMutex*()? At first glance, I'm not enthused to add an OS specific extension to an OS independent API?

review: Needs Fixing
Revision history for this message
mdavidsaver (mdavidsaver) wrote :

> ... still uses non-PI mutexes in some other places ...

Ah, I see. pthread_mutex_init() currently appears in posix/osdEvent.c, osdMutex.c, osdSpin.c, and osdThread.c.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

> I tried the simply minded (early morning) test of removing the two #undef lines.

Turns out I have a quick answer for this. Some ~emulated environments don't fully support the PI futex() operations. (eg. rr-project.org) In these cases pthread_mutex_init() fails with ENOTSUP.

It seems like the right approach is to probe, as is done in osdThread.c, to see if PI mutex is available.

Revision history for this message
Andrew Johnson (anj) wrote :

Names beginning with epics are generally intended for public use, which the epicsPosixMutex stuff shouldn't be. If I've understood the code correctly you aren't replacing the pthread_mutex_t type but are providing a wrapper for pthread_mutex_init() that creates one, and you're also moving the need for the caller to set mutex attributes on the new mutex. Any new publicly visible symbols that are meant to be internal to a libCom implementation should begin with "osd", so maybe osd_mutex_init()?

I've read the essay in the comments just below epicsMutexOsdShow(), but I'm not sure how it helps future maintainers to understand this file. The discussion there doesn't seem appropriate in source code comments.

Is the new test program generic? It has some CPU-affinity stuff in it which is conditional on glibc; what happens on other OSs (Windows, MacOS, RTEMS-5, embedded Linux) that can also do SMP and don't use glibc?

I'm not sure I like the new environment variable or understand what effect not defining it will have. Is there a good reason to continue to support the previous mode if in practice it's broken? As long as we document the changes in behavior when SCHED_FIFO is available (I'm assuming nothing should change when it isn't) I'm not convinced we need the env switch, which would resolve Dirk's question about how to set an environment variable before running a test program.

However I wonder if it would be desirable to run this new test (and maybe some of the other thread-related test programs too) twice, once with the default scheduler and once under SCHED_FIFO?

Finally I think we would probably want to delay merging this until after Heinz' RTEMS-5 code has been pulled in, that might require some changes to this MR because he uses the Posix APIs in the new RTEMS port.

review: Needs Fixing
Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

> Also, what is the motivation to economize pthread_mutexattr_t? I see
> __SIZEOF_PTHREAD_MUTEXATTR_T==4 (on a 64-bit host).

It's simply not necessary that each mutex creates its own attribute.
From the pthread_mutexattr_init manual page:
"After a mutex attributes object has been used to initialize one or more mutexes, any function affecting the attributes object (including destruction) shall not affect any previously initialized mutexes."
Thus the same attribute can be used for all mutexes and the mutex does not use it any more after creation. It is a waste of not only space but also time and error handling complexity to have (and keep!) one in each mutex.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

> ... intended for public use, which the epicsPosixMutex stuff shouldn't be ...

Ok, as internal helpers, calls like these (with an ANJ compliant name) are perfectly reasonable. I would like to see them moved to a private header. Maybe 'os/posix/osdMutexPvt.h'?

> ... I'm not convinced we need the env switch ...

Likewise. We already have the build time $(USE_POSIX_THREAD_PRIORITY_SCHEDULING) switch.

> ... I'm not sure how it helps future maintainers to understand this file ...

I'll go further. In-code comments should provide meaningful context for the code around them. This discussion would make a nice core-talk (or maybe blog) post, but is a distraction here.

> ... I think we would probably want to delay merging this until after Heinz' RTEMS-5 code has been pulled in ...

I don't like this. To my mind, we merge whichever is ready first, and the second needs to be reworked to resolve any conflicts. I think this gives a health incentive for promptness.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

> Also, what is the motivation to economize pthread_mutexattr_t?

Should I make a separate merge request that just handles the removal of per-mutex attributes?
This may allow to merge the cleanup before before agreeing on the new features.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

I think a smaller diff would help move the process faster, as would more targeted changes.

Personally, I'm not sure I'll accept a global pthread_mutexattr_t. I think this should become a local in epicsMutexOsdCreate() (or equivalent helper).

As a general rule, I prefer locality for various reasons. Modern processors with deep caches generally reward memory locality. Mostly though, it is a case of readability. I find it easier to understand code which doesn't reference globals.

Revision history for this message
Andrew Johnson (anj) wrote :

Ack on Michael's private header idea since I believe these definitions should only be needed when compiling the code in libCom itself, not by the client application.

Looking at the code again, there is only one call to epicsPosixMutexMustInit(), from a place where it isn't actually needed. The epicsMutexOsdCreate() routine is supposed to return NULL if it can't finish (not suspend the thread) so calling a MustInit() routine there is wrong and the code for it can be removed.

Similarly there is only one call to epicsPosixMutexAttrGet(), from the epicsPosixMutexInit() routine. The two pthread_mutexattr_t objects are really just constants passed into pthread_mutex_init() that we have to look up the values of at run-time, and they aren't globally visible anyway. The osd_mutex_init() routine should create a static instance the first time each one is asked for, or create both the first time it's called. Till's use of pthread_once() is fine for doing that, but moving it into the _init() routine would be better.

Hmm, according to the online spec (and both manpages I just checked) passing a NULL attribute into pthread_mutex_init() is legal and is equivalent to a default-initialized mutex, so we only actually need to create the recursive one.

I'm also not terribly keen on adding our own enum for selecting the mutex type either, why not just use the mutex type constants PTHREAD_MUTEX_DEFAULT and PTHREAD_MUTEX_RECURSIVE as the second parameter to osd_mutex_init() to switch on? The pthread.h header is required to define those, and to be honest I don't believe that any of our supported targets don't support both (does anyone know of a supported Posix target that doesn't have recursive mutexes?). I wonder how much of the other conditional code could we drop too, just putting a check somewhere so the build will fail on any Posix target that doesn't provide the features we need?

We don't support Solaris any more (especially Solaris 8), so those comments can go too.

I withdraw my "after RTEMS-5" suggesting, if this becomes ready first it should get merged. It looks like the two won't conflict anyway though.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

I am taking the changes apart now...

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

> Names beginning with epics are generally intended for public use, which the
> epicsPosixMutex stuff shouldn't be. If I've understood the code correctly you
> aren't replacing the pthread_mutex_t type but are providing a wrapper for
> pthread_mutex_init() that creates one, and you're also moving the need for the
> caller to set mutex attributes on the new mutex. Any new publicly visible
> symbols that are meant to be internal to a libCom implementation should begin
> with "osd", so maybe osd_mutex_init()?

I think something with "posix" in the name would be good as it only affects posix mutexes?
But maybe Till had more in mind: An OSI interface to ask for recursive or (maybe cheaper) non-recursive mutexes, no matter how they are implemented and what the OS is. Could be semM vs semB in vxworks or whatever in Windows. Of course the header should not be "osd" in that case.
Is there a need to ask for (possibly cheaper) non-recursive mutexes? They are now only used in osdSpin.c (only if no posix spin locks are available), in osdEvent.c and osdThread.c. Everything else uses recursive mutexes.

>
> I've read the essay in the comments just below epicsMutexOsdShow(), but I'm
> not sure how it helps future maintainers to understand this file. The
> discussion there doesn't seem appropriate in source code comments.

I will remove the essay.

>
> Is the new test program generic? It has some CPU-affinity stuff in it which is
> conditional on glibc; what happens on other OSs (Windows, MacOS, RTEMS-5,
> embedded Linux) that can also do SMP and don't use glibc?

The change is in posix only, not Windows or RTEMS. The test is Linux only. Maybe it should be posix only as well. The CPU affinities are used to guarantee CPU starvarion of the high priority thread on multi-core systems as well. Otherwise the test may show a false success. Should be possible to run the test on all posix architectures.

Do we need a cpu affinity OSI API?

>
> I'm not sure I like the new environment variable or understand what effect not
> defining it will have. Is there a good reason to continue to support the
> previous mode if in practice it's broken? As long as we document the changes
> in behavior when SCHED_FIFO is available (I'm assuming nothing should change
> when it isn't) I'm not convinced we need the env switch, which would resolve
> Dirk's question about how to set an environment variable before running a test
> program.

The previous implementation is a bit "faster on average" because the mutexes are simpler. But "fast" only counts in RT context, and there "faster on average" basically means "wrong".

I can take the environment variable out. It was in so we can easily switch back and forth to see the difference. And as an anti change-panic device.

>
> However I wonder if it would be desirable to run this new test (and maybe some
> of the other thread-related test programs too) twice, once with the default
> scheduler and once under SCHED_FIFO?

You mean expected failure is success? How to program that?

9f08d6d... by Dirk Zimoch

removed the in-code essay

45856b0... by Dirk Zimoch

remove epicsMutexMustInit, instead return NULL on failure

5ee60ae... by Dirk Zimoch

remove epicsPosixMutexAttrGet() enum EpicsPosixMutexProperty

b4afc97... by Dirk Zimoch

rename epicsPosixMutexInit to osdPosixMutexInit

Revision history for this message
Andrew Johnson (anj) wrote :

> I think something with "posix" in the name would be good as it only affects posix mutexes?

Whether a mutex is "posix" or not is a characteristic of the target being built for, not the kind of mutex being created. There are no posix mutexes for our Windows or VxWorks implementations so that makes no sense to me. I suggested using a more pthreads-style name osd_mutex_init() because the thing it returns is an actual pthread_mutex_t which gets passed into other pthread_*() routines. Passing something apparently created by a non-pthreads routine into a pthreads routine is a bit of a code smell, it looks wrong even though it isn't really.

> You mean expected failure is success? How to program that?

There are some problems with the test program; it always defines TEST_PLAN as 8, but the number of test results that will be reported will not be the same for all builds. There are several tests which won't report any result if HAVE_CPU_AFFINITY is undefined. This situation is why the unitTest API provides testSkip(), which lets a program declare that it can't run some number of tests in its current configuration or on this target. That is how running the same test with both schedulers would work, at run-time the code detects whether the tests should pass or not and calls testSkip() instead if not. Please don't try to adjust the test plan at runtime instead, it's much easier to read test code if a particular group of tests always generates the same number of results.

My suggestion of running the thread test programs a second time on Linux was really an idea for Michael and Ralph to consider: On the CI runners that give us sudo it ought to be possible to configure our account's maxrtprio hard limit somewhere in the job setup, then we would add linux-only scripts such as epicsThreadPriorityRT.plt which run the program again using say
        prlimit -r 80 ./epicsThreadPriority

b611d31... by Dirk Zimoch

moved osdPosixMutexInit to private header

e41fa06... by Dirk Zimoch

remove check of environment variable EPICS_MUTEX_USE_PRIORITY_INHERITANCE

20c0dba... by Dirk Zimoch

removed comment about outdated solaris version

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Is there any posix implementation which does not support PTHREAD_MUTEX_RECURSIVE? That would be systems with _XOPEN_SOURCE < 500, that is Posix 1995.

I mean: do we still need the alternative implementation?

I have 12 different Linux architectures, some quite old, and none uses that compilation path.

If we do, should _XOPEN_SOURCE be the thing to be tested or rather #ifdef PTHREAD_MUTEX_RECURSIVE ?

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

> If we do, should _XOPEN_SOURCE be the thing to be tested or rather #ifdef
> PTHREAD_MUTEX_RECURSIVE ?

I just see that PTHREAD_MUTEX_RECURSIVE is an enum, not a #define. Thus, no way to check it with #ifdef.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Found a problem in one of my earlier changes. Will changes that and force-push soon....

f2bd8ec... by Dirk Zimoch

PTHREAD_MUTEX_NORMAL may not be defined when Posix version is too old (before 1995)

3c7596d... by Dirk Zimoch

removed unreachable code

7e1812e... by Dirk Zimoch

added missing macro in case USE_POSIX_THREAD_PRIORITY_SCHEDULING=NO

5703c77... by Dirk Zimoch

removed warning which interfered with tests

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Problem was not as big as I first thought. No force-push necessary.

Still the question remains: Sould we support _XOPEN_SOURCE < 500 ?

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

I'm inclined to say no. Of course our problem is how to decide.

Technically, I find references in 'man feature_test_macros' to _XOPEN_SOURCE>=600 being recognized by glibc 2.2 (circa 2000). So at least on Linux I think the <500 case stopped being used years ago. Since we no longer support solaris, that leaves the *BSDs, and OSX.

Practically, if the change passes the Linux and OSX CI builders, I'd be happy to accept the simplification.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

Also, it may be worth repeating. Even on modern Linux, support for PTHREAD_PRIO_INHERIT can't be taken for granted as some (partially) emulated environments don't implement/allow all of the syscalls necessary for PI mutex operations.

So I think it is necessary to probe for support at runtime. eg. with glibc, an error will be returned by pthread_mutex_init().

The obvious place to do this would be in your thread once function. Try to create a mutex with PTHREAD_PRIO_INHERIT, the fallback to PTHREAD_PRIO_NONE if this isn't possible. This would also help convince me that a thread once is the better approach.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Sure, run time failure needs to be considered. Falling back from PTHREAD_PRIO_INHERIT to PTHREAD_PRIO_NONE is relatively safe.

What we really need is recursive mutexes. Removing the pre 1995 compatible code (older than VxWorks 5.5, yay) would simplify the implementation a lot. As long as we document the drop of support, I think we are fine.

4039d97... by Dirk Zimoch

drop workaround for missing PTHREAD_MUTEX_RECURSIVE support

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

I have now dropped the workaround for missing PTHREAD_MUTEX_RECURSIVE in pre 2001 Posix.

Falling back from PTHREAD_PRIO_INHERIT to PTHREAD_PRIO_NONE is still supported of course. Line 60 pthread_mutexattr_settype() will fail but that will simply result in a default mutex attribute and an error message (if errVerbose is set).

Setting PTHREAD_MUTEX_RECURSIVE in line 70 must not fail. That should not be a problem, even for rr or other special systems.

The only new external function is now called osdPosixMutexInit (not starting with "epics") and is in the private header file osdPosixMutexPriv.h which does not get installed. It is used inside libCom only.

I am not yet happy with the test. I need to find out how to use testSkip() properly when SCHED_FIFO is not available or the prority range is too limited.

There seems to be no TESTPROD_HOST_Posix (instead of TESTPROD_HOST_Linux) to run the test on all posix architectures. How to do that in the Makefile?

4197954... by Dirk Zimoch

remove unnecessary setAttrDefaults() function

6d54127... by Dirk Zimoch

run epicsMutexPriorityInversionTest for all Posix architectures

Revision history for this message
Andrew Johnson (anj) wrote :

A couple more small changes that I noticed last week (see my inline comments), these may be the last changes we ask for – thanks!

9eda0c0... by Dirk Zimoch

removed useless epicsShare macros from osdPosixMutexInit

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Done. I would like to fix the test so that it does not complain when RT prios are not available....

f003ed6... by Dirk Zimoch

Do not fail (but skip) epicsMutexPriorityInversionTest when SCHED_FIFO is unavailable

03b909b... by Dirk Zimoch

do not count as test that posix functions are actually working

d5dbd83... by Dirk Zimoch

run epicsMutexPriorityInversionTest even when CPU affinity is not available for old glibc because those systems likely have only one CPU core

a09e429... by Dirk Zimoch

relax epicsMutexPriorityInversionTest timing a bit

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

I think this is done.

review: Approve
Revision history for this message
mdavidsaver (mdavidsaver) wrote :

I pushed your latest to github for testing. There are a couple of failures with RTEMS and OSX.

https://github.com/mdavidsaver/epics-base/actions/runs/430893895

I still think it is necessary for the new globalAttrInit() to probe for runtime support of PTHREAD_PRIO_INHERIT. I've included a sketch of this as a diff comment.

Unless I'm misreading, it looks like only the recursive implementation would have PTHREAD_PRIO_INHERIT set. So the mutex used internally in osdThread and osdSpin would continue to be non-PI.

review: Needs Fixing
Revision history for this message
Heinz Junkes (junkes) wrote :

Yes, even RTEMS5 without the new libbsd-network-stack has not defined _XOPEN_SOURCE and asks for the old implementation. I can set _XOPEN_SOURCE by hand and it works so far with it. But what I noticed in this context and I think that the mutexes may be used incorrectly in some modules and therefore it comes to problems such as: "mutex not owned by the task".

I saw the whole thing in the context of MotorRecords (OMS):

https://github.com/epics-modules/motor/issues/172

I see a general problem that probably in some places mutexes are created in the iocInit task and then used afterwards in other tasks although the iocInit task has been terminated.

As I understand it, mutexes should generally be created in the tasks in which they are also used? Or with shared memory the creating task must remain however existing?

Revision history for this message
Heinz Junkes (junkes) wrote :

> Yes, even RTEMS5 without the new libbsd-network-stack has not defined
> _XOPEN_SOURCE and asks for the old implementation. I can set _XOPEN_SOURCE by
> hand and it works so far with it. But what I noticed in this context and I
> think that the mutexes may be used incorrectly in some modules and therefore
> it comes to problems such as: "mutex not owned by the task".
>
> I saw the whole thing in the context of MotorRecords (OMS):
>
> https://github.com/epics-modules/motor/issues/172
>
> I see a general problem that probably in some places mutexes are created in
> the iocInit task and then used afterwards in other tasks although the iocInit
> task has been terminated.
>
> As I understand it, mutexes should generally be created in the tasks in which
> they are also used? Or with shared memory the creating task must remain
> however existing?

If you build RTEMS with the POSIX_FLAGS like in Linux (-D_GNU_SOURCE -D_DEFAULT_SOURCE) then with the help of includes/sys/features.h _XOPENS_SOURCE is defined to 700.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Now I have some time again to return to this one...

Michael, if I understand correctly, you would like to see a run-time test that the attribute PTHREAD_PRIO_INHERIT can actually be used to init a mutex? Have you really seen that pthread_mutexattr_setprotocol(&globalAttrRecursive, PTHREAD_PRIO_INHERIT) succeeds but the resulting globalAttrRecursive cannot be used to init a mutex? If yes, I find that behavior strange and unexpected.

Heinz, do you think I should add #define D_GNU_SOURCE somewhere in the code or should that be done in the configuration?

Concerning "mutex not owned by task": As a mutex is used to synchronize different tasks, there is no specific task "owning" it permanently. The task that has last locked it owns it, independent of the creator task. The only situation where I would expect such a message is if task A has locked it but task B tries to unlock it. Or no task has locked it but now it is unlocked. That has nothing to do which who created it. The problem in the MotorRecord originated in the wrong check of TryLock and thus a confusion which task owned the mutex at run time.

30a2421... by Dirk Zimoch

check at run-time if PTHREAD_PRIO_INHERIT actually works

b0f1271... by Dirk Zimoch

define _GNU_SOURCE for all platforms to make sure all needed posix functions are available

855519c... by Dirk Zimoch

No need to include features.h directly. And may fail on some OS.

Revision history for this message
Heinz Junkes (junkes) wrote :

I added the following to RTEMS:
in configure/os/CONFIG.Common.RTEMS
POSIX_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE

Just like it is already done for Linux:
in configure/os/CONFIG.COMMON.linuxCommon you can find
# Define _GNU_SOURCE and _DEFAULT_SOURCE for maximum portability
POSIX_CPPFLAGS = -D_GNU_SOURCE -D_DEFAULT_SOURCE

> Now I have some time again to return to this one...
>
> Michael, if I understand correctly, you would like to see a run-time test that
> the attribute PTHREAD_PRIO_INHERIT can actually be used to init a mutex? Have
> you really seen that pthread_mutexattr_setprotocol(&globalAttrRecursive,
> PTHREAD_PRIO_INHERIT) succeeds but the resulting globalAttrRecursive cannot be
> used to init a mutex? If yes, I find that behavior strange and unexpected.
>
> Heinz, do you think I should add #define D_GNU_SOURCE somewhere in the code or
> should that be done in the configuration?
>
> Concerning "mutex not owned by task": As a mutex is used to synchronize
> different tasks, there is no specific task "owning" it permanently. The task
> that has last locked it owns it, independent of the creator task. The only
> situation where I would expect such a message is if task A has locked it but
> task B tries to unlock it. Or no task has locked it but now it is unlocked.
> That has nothing to do which who created it. The problem in the MotorRecord
> originated in the wrong check of TryLock and thus a confusion which task owned
> the mutex at run time.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

> ... Have you really seen ...

Yes Dirk, really really. cf. https://github.com/rr-debugger/rr/issues/2750

If we open the source it's easy enough to see what glibc (at least) is doing.

https://github.com/bminor/glibc/blob/76ea70c613cee23a1846b9605e6433c1fa8baea7/nptl/pthread_mutexattr_setprotocol.c#L25

So pthread_mutexattr_setprotocol() will only fail if passed an unknown flag.

https://github.com/bminor/glibc/blob/76ea70c613cee23a1846b9605e6433c1fa8baea7/nptl/pthread_mutex_init.c#L76-L78

And here is pthread_mutex_init() testing for OS support, and returning ENOTSUP if this isn't found.

If anything, I'm left with some gratitude that glibc does this test in pthread_mutex_init() instead of letting it be discovered during pthread_mutex_lock().

Also, seeing how pthread_mutexattr_t is only a bit mask makes me skeptical of economizing.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

I have now added the run-time test, removed the unnecessary direct include of features.h and added _GNU_SOURCE to the source code. I am now rebasing everything onto the current 7.0.

b410ce8... by Dirk Zimoch

don't destroy mutex when init failed

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

I think this leaves only the question of osdPosixMutexInit() and PTHREAD_MUTEX_DEFAULT. As it stands now, the mutexs created through epicsMutex can have PI enabled, but those created elsewhere (eg. osdSpin) will not.

wrt. _GNU_SOURCE I don't think this should be defined in osdMutex.c, or any source file. This is easily removed later though. Also, it is my turn to be surprised to see #ifdef _GNU_SOURCE appearing in RTEMS (which uses newlib, not glibc).

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

> As it stands now, the mutexs created through epicsMutex can have PI enabled, but those created elsewhere (eg. osdSpin) will not.

Oops, you are right. I got confused by Andrew's earlier remark:

> Hmm, according to the online spec (and both manpages I just checked) passing a NULL attribute into pthread_mutex_init() is legal and is equivalent to a default-initialized mutex, so we only actually need to create the recursive one.

Of course, a non-recursive mutex with PI enabled is not a default mutex and we need a mutex attribute for those.

53fcc93... by Dirk Zimoch

fix bug in commit 5ee60a: We need a mutex attribute for non-recursive, PI enabled mutexes

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Concerning _GNU_SOURCE, this is what gnu.org has to say about this:

"You should define these macros by using ‘#define’ preprocessor directives at the top of your source code files. These directives must come before any #include of a system header file. It is best to make them the very first thing in the file, preceded only by comments. You could also use the ‘-D’ option to GCC, but it’s better if you make the source files indicate their own meaning in a self-contained way."

So better put it in the source files, not in the Makefile.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

Ok, I think this covers all of the issues which I can see.

I've pushed your latest to my github to trigger CI builds, and a couple are failing with your favorite implicit declaration error.

https://github.com/mdavidsaver/epics-base/actions/runs/528387798

> ../epicsMutexPriorityInversionTest.c:87: error: implicit declaration of function 'pthread_getschedparam'

I think that explicitly including pthread.h from epicsMutexPriorityInversionTest.c should resolve this.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

The failing MacOS compilation have this error:
  ../epicsMutexPriorityInversionTest.c:19:54: error: function-like macro '__GLIBC_PREREQ' is not defined
  #if defined(__GLIBC__) && defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2,4)

It first tests that __GLIBC_PREREQ is defined and then caomplains that it is not defined?
Is it defined as a non-function-like macro on that platform?

The RTEMS compilations fail in the same line but with another error:
  ../epicsMutexPriorityInversionTest.c:19:68: error: missing binary operator before token "("

Again, it seems to have problems with __GLIBC_PREREQ.

The C Processor manual says that && and || "obey the usual short-circuiting rules of standard C." Thus __GLIBC_PREREQ(2,4) should never be evalueted if __GLIBC_PREREQ is not defined.

Anyway, I will try to fix this...

a97e5af... by Dirk Zimoch

Do not rely on __GLIBC_PREREQ macro

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

The CI environments should make sure to run the test with sufficient privileges to use RT priorities.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

... __GLIBC_PREREQ

Ah, sorry I didn't catch this. There is a difference in how CPP treats undefined "variable" vs. "function" macros. eg. an undefined __GNUC__ will silently expand to zero. An undefined __GLIBC_PREREQ() is an error.

What you have is fine, and no further change is needed. So I think this change is now ready to merge.

I will note that this could also be expressed using the VERSION_INT() macro from epicsVersion.h as

> #ifdef __GLIBC__
> #define GLIBC_VERSION VERSION_INT(__GLIBC__, __GLIBC_MINOR__, 0, 0)
> #endif

and then

> #if GLIBC_VERSION >= VERSION_INT(2,4,0,0)

This works since an undefined GLIBC_VERSION is treated as a zero.

> ... The CI environments should ...

This is currently being controlled by '.github/workflows/ci-scripts-build.yml'. As a first pass, I should be sufficient to apply 'sudo' to the following line.

> run: python .ci/cue.py test

review: Approve
Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Ohhh, I still have a typo:
if __GLIBC__ * 100 * __GLIBC_MINOR__ >= 204

should be + __GLIBC_MINOR__ of course.

b324abe... by Dirk Zimoch

fix typo in GLIBC version check

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

> ... I think this change is now ready to merge.

... once the CI builds pass.

https://github.com/mdavidsaver/epics-base/runs/1816607672?check_suite_focus=true

RTEMS 4.10

  ../epicsMutexPriorityInversionTest.c:87: error: implicit declaration of function 'pthread_self'

Missing an include of <pthread.h>

OSX

  ../epicsMutexPriorityInversionTest.c:226:10: error: implicit declaration of function 'sched_setscheduler' is invalid in C99 [-Werror,-Wimplicit-function-

Not sure about this one as <sched.h> is being included. Can someone with a Mac see if/where this is defined?

review: Needs Fixing
Revision history for this message
mdavidsaver (mdavidsaver) wrote :

Ok, maybe time to take a(nother) step back and ask if epicsMutexPriorityInversionTest should be run by default.

> testOk1( hiPriStalledTimeUs < 1000 );

This will almost certainly fail randomly with the CI builders since they don't have a PREEMPT_RT kernel, and are anyway timeshare VMs.

I don't see a need for Base to test whether PTHREAD_PRIO_INHERIT functions correctly. This seems like testing malloc(). The critical question which a user will have is whether PTHREAD_PRIO_INHERIT is being set. This seems more like asking whether OS priorities are being set at all. This is complicated since, unlike priority or mlock(), I don't know of a straightforward way to verify this externally. (I would resort to strace, which is complicated to interpret)

The question of OS priorities is answered by epicsThreadShowAll(). So maybe it would be sufficient for epicsMutexShowAll() to print a line showing if PTHREAD_PRIO_INHERIT is being set?

Revision history for this message
Andrew Johnson (anj) wrote :

OSX:

woz$ man sched_setscheduler
No manual entry for sched_setscheduler

This routine doesn't exist on macOS (Google confirms).

5ff7658... by Dirk Zimoch

add missing pthread.h

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Does that mean that OSX cannot use RT priorities?

786de29... by Dirk Zimoch

run epicsMutexPriorityInversionTest only if we have priority scheduling enabled

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

I have modified the Makefilt to run (even compile) the test only if priority scheduling is available. Otherwise the test would be rather pointless.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

The OSX build still fails.

https://github.com/mdavidsaver/epics-base/actions/runs/534660122

fyi. Base uses the pthread_setschedparam() instead of sched_setscheduler(). Combining with pthread_self() should accomplish what you intend.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Base also uses sched_setscheduler() in modules/libcom/src/osi/os/posix/osdProcess.c line 104. How does that work with OSX?

4855d9e... by Dirk Zimoch

use pthread_setschedparam() instead of sched_setscheduler() because of OSX

Revision history for this message
Andrew Johnson (anj) wrote :

#define _POSIX_THREAD_PRIORITY_SCHEDULING (-1) /* [TPS] */

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

https://github.com/mdavidsaver/epics-base/runs/1856569036?check_suite_focus=true

Progress. Everything builds, but the new epicsMutexPriorityInversionTest fails of OSX. Of course, it only "succeeds" on the Linux builds due to lack of permission.

I think this change is basically done and ready to go except for the new test. Is this test so essential that it needs to be an obstacle to merging?

Maybe it would be sufficient for epicsMutexShowAll() to print a line showing if PTHREAD_PRIO_INHERIT is being set?

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

@Dirk, unless you object, I'm going to merge this work excluding epicsMutexPriorityInversionTest. This test can be added later when it has been perfected.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Feel free to remove the test.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

I squashed the series and applied it as 5a8b6e4111b869e8913b7bfaff4c56b546fb8879, I also changed epicsMutexShowAll() to print "PI is enabled" if mutexes are being created with PTHREAD_PRIO_INHERIT.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Unfortunately you introduced a bug in epicsMutexOsdShowAll(). Not all implementations have pthread_mutexattr_getprotocol and PTHREAD_PRIO_INHERIT. Earlier in that file you find `#if defined _POSIX_THREAD_PRIO_INHERIT`. The same is needed here.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

Corrected with 3c46542630823a272001aaab4e6fc265c7e03046.

Which targets don't define _POSIX_THREAD_PRIO_INHERIT?

review: Approve
Revision history for this message
Andrew Johnson (anj) wrote :

There's also an issue that Brendan found and fixed in his rtems5 branch, the assumption in calls to osdPosixMutexInit() in osdEvent.c, osdSpin.c and osdThread.c that PTHREAD_MUTEX_DEFAULT always has the value 0. That fix will come in when we merge his branch though, so it probably isn't necessary to cherry-pick it into 7.0.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

To expand, I take it you're referring to the change which replaces

> osdPosixMutexInit(&spin->lock, 0);

calls with

> osdPosixMutexInit(&spin->lock, PTHREAD_MUTEX_DEFAULT);

Where RTEMS 5 has

> .../include/sys/_pthreadtypes.h:#define PTHREAD_MUTEX_DEFAULT 3

This is a simple enough change, and the exact timetable for RTEMS5 support is uncertain enough to me, that I think it should be applied on 7.0 soon. Do you (ANJ) want to do this? If not, then I will.

Revision history for this message
Andrew Johnson (anj) wrote :

Yup. I was assuming that no other target would need those changes before RTEMS-5 since our CI jobs haven't failed, although I guess freebsd might (could we add that to our GitHub-Actions jobs?). I'm a bit snowed under right now so if you think this needs to go in before the rest of RTEMS-5 go ahead and cherry-pick Brendan's change.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

Looking at the other open libc impls. uclibc has almost the same enum{} as glibc, so PTHREAD_MUTEX_DEFAULT is zero. musl explicitly defines PTHREAD_MUTEX_DEFAULT as zero. It looks like newlib is the exception.

> ... since our CI jobs haven't failed ...

This would depend on what 0 actually means.

> ... could we add that to our GitHub-Actions jobs?

Maybe? This will probably wait for someone with sufficient interest in epics on *BSD.

> ... go ahead and cherry-pick Brendan's change.

Ok. Simple as it may be, I'll wait for a CI run before pushing to the 7.0 branch.

https://github.com/mdavidsaver/epics-base/actions/runs/615581362

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Michael, the target without _POSIX_THREAD_PRIO_INHERIT is some old embedded Linux based on Montavista Linux 4.0 that came with a serial interface box from Moxa.

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

I am now testing the PTHREAD_MUTEX_DEFAULT fix ....

Revision history for this message
Dirk Zimoch (dirk.zimoch) wrote :

Compiles for all my targets.

Revision history for this message
mdavidsaver (mdavidsaver) wrote :

Not directly related to Dirk's change, but I want to note it somewhere. The fallback implementation in posix/epicsAtomicOSD.cpp is using a statically initialized, not RT, mutex. Obviously only relevant to older posix targets without atomic builtins.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/documentation/RELEASE_NOTES.md b/documentation/RELEASE_NOTES.md
2index 1679526..1ef81a4 100644
3--- a/documentation/RELEASE_NOTES.md
4+++ b/documentation/RELEASE_NOTES.md
5@@ -17,6 +17,7 @@ should also be read to understand what has changed since earlier releases.
6
7 <!-- Insert new items immediately below here ... -->
8
9+<<<<<<< documentation/RELEASE_NOTES.md
10
11
12 ### Build System: New `VALID_BUILDS` type "Command"
13@@ -36,6 +37,14 @@ cross-compiled targets though since `CONFIG.Common.UnixCommon` sets it.
14 The other `VALID_BUILDS` types are "Host" for target architectures that can
15 compile and run their own programs (`PROD_HOST` etc.), and "Ioc" for targets
16 that can run IOCs (`PROD_IOC` etc.).
17+=======
18+### Priority inversion safe posix mutexes
19+
20+On Posix systems, epicsMutex now support priority inheritance if available.
21+The IOC needs to run with SCHED_FIFO engaged.
22+Support for Posix implementations before POSIX.1-2001 (_XOPEN_SOURCE < 500,
23+glibc version < 2.3.3) has been dropped.
24+>>>>>>> documentation/RELEASE_NOTES.md
25
26 ### Support for JSON5
27
28diff --git a/modules/libcom/src/osi/Makefile b/modules/libcom/src/osi/Makefile
29index f0108e6..ce9dcc6 100644
30--- a/modules/libcom/src/osi/Makefile
31+++ b/modules/libcom/src/osi/Makefile
32@@ -112,6 +112,7 @@ Com_SRCS += osdStdio.c
33 THREAD_CPPFLAGS_NO += -DDONT_USE_POSIX_THREAD_PRIORITY_SCHEDULING
34 osdThread_CPPFLAGS += $(THREAD_CPPFLAGS_$(USE_POSIX_THREAD_PRIORITY_SCHEDULING))
35 osdSpin_CPPFLAGS += $(THREAD_CPPFLAGS_$(USE_POSIX_THREAD_PRIORITY_SCHEDULING))
36+osdMutex_CPPFLAGS += $(THREAD_CPPFLAGS_$(USE_POSIX_THREAD_PRIORITY_SCHEDULING))
37
38 Com_SRCS += osdThread.c
39 Com_SRCS += osdThreadExtra.c
40diff --git a/modules/libcom/src/osi/os/posix/osdEvent.c b/modules/libcom/src/osi/os/posix/osdEvent.c
41index 251f35d..db61c24 100644
42--- a/modules/libcom/src/osi/os/posix/osdEvent.c
43+++ b/modules/libcom/src/osi/os/posix/osdEvent.c
44@@ -23,6 +23,7 @@
45 #include "epicsEvent.h"
46 #include "epicsTime.h"
47 #include "errlog.h"
48+#include "osdPosixMutexPriv.h"
49
50 struct epicsEventOSD {
51 pthread_mutex_t mutex;
52@@ -50,11 +51,11 @@ LIBCOM_API epicsEventId epicsEventCreate(epicsEventInitialState init)
53 epicsEventId pevent = malloc(sizeof(*pevent));
54
55 if (pevent) {
56- int status = pthread_mutex_init(&pevent->mutex, 0);
57+ int status = osdPosixMutexInit(&pevent->mutex, 0);
58
59 pevent->isFull = (init == epicsEventFull);
60 if (status) {
61- printStatus(status, "pthread_mutex_init", "epicsEventCreate");
62+ printStatus(status, "osdPosixMutexInit", "epicsEventCreate");
63 } else {
64 status = pthread_cond_init(&pevent->cond, 0);
65 if (!status)
66diff --git a/modules/libcom/src/osi/os/posix/osdMutex.c b/modules/libcom/src/osi/os/posix/osdMutex.c
67index 707ae63..6279c57 100644
68--- a/modules/libcom/src/osi/os/posix/osdMutex.c
69+++ b/modules/libcom/src/osi/os/posix/osdMutex.c
70@@ -11,6 +11,10 @@
71
72 /* Author: Marty Kraimer Date: 13AUG1999 */
73
74+#ifndef _GNU_SOURCE
75+#define _GNU_SOURCE
76+#endif
77+
78 #include <stddef.h>
79 #include <stdlib.h>
80 #include <stdio.h>
81@@ -21,6 +25,7 @@
82 #include <pthread.h>
83
84 #include "epicsMutex.h"
85+#include "osdPosixMutexPriv.h"
86 #include "cantProceed.h"
87 #include "epicsTime.h"
88 #include "errlog.h"
89@@ -38,6 +43,69 @@
90 cantProceed((method)); \
91 }
92
93+#if defined(DONT_USE_POSIX_THREAD_PRIORITY_SCHEDULING)
94+#undef _POSIX_THREAD_PRIO_INHERIT
95+#endif
96+
97+/* Global var - pthread_once does not support passing args but it is more efficient
98+ * than epicsThreadOnce which always acquires a mutex.
99+ */
100+static pthread_mutexattr_t globalAttrDefault;
101+static pthread_mutexattr_t globalAttrRecursive;
102+static pthread_once_t globalAttrInitOnce = PTHREAD_ONCE_INIT;
103+
104+static void globalAttrInit()
105+{
106+ int status;
107+
108+ status = pthread_mutexattr_init(&globalAttrDefault);
109+ checkStatusQuit(status,"pthread_mutexattr_init(&globalAttrDefault)","globalAttrInit");
110+ status = pthread_mutexattr_init(&globalAttrRecursive);
111+ checkStatusQuit(status,"pthread_mutexattr_init(&globalAttrRecursive)","globalAttrInit");
112+ status = pthread_mutexattr_settype(&globalAttrRecursive, PTHREAD_MUTEX_RECURSIVE);
113+ checkStatusQuit(status, "pthread_mutexattr_settype(&globalAttrRecursive, PTHREAD_MUTEX_RECURSIVE)", "globalAttrInit");
114+#if defined _POSIX_THREAD_PRIO_INHERIT
115+ status = pthread_mutexattr_setprotocol(&globalAttrDefault, PTHREAD_PRIO_INHERIT);
116+ if (errVerbose) checkStatus(status, "pthread_mutexattr_setprotocol(&globalAttrDefault, PTHREAD_PRIO_INHERIT)");
117+ status = pthread_mutexattr_setprotocol(&globalAttrRecursive, PTHREAD_PRIO_INHERIT);
118+ if (errVerbose) checkStatus(status, "pthread_mutexattr_setprotocol(&globalAttrRecursive, PTHREAD_PRIO_INHERIT)");
119+ if (status == 0) {
120+ /* Can we really use PTHREAD_PRIO_INHERIT? */
121+ pthread_mutex_t temp;
122+ status = pthread_mutex_init(&temp, &globalAttrRecursive);
123+ if (errVerbose) checkStatus(status, "pthread_mutex_init(&temp, &globalAttrRecursive)");
124+ if (status != 0) {
125+ /* No, PTHREAD_PRIO_INHERIT does not work, fall back to PTHREAD_PRIO_NONE */;
126+ pthread_mutexattr_setprotocol(&globalAttrDefault, PTHREAD_PRIO_NONE);
127+ pthread_mutexattr_setprotocol(&globalAttrRecursive, PTHREAD_PRIO_NONE);
128+ } else {
129+ pthread_mutex_destroy(&temp);
130+ }
131+ }
132+#endif
133+}
134+
135+int osdPosixMutexInit (pthread_mutex_t *m, int mutextype)
136+{
137+ pthread_mutexattr_t *atts;
138+ int status;
139+
140+ status = pthread_once( &globalAttrInitOnce, globalAttrInit );
141+ checkStatusQuit(status,"pthread_once","epicsPosixMutexAttrGet");
142+
143+ switch (mutextype) {
144+ case PTHREAD_MUTEX_DEFAULT:
145+ atts = &globalAttrDefault;
146+ break;
147+ case PTHREAD_MUTEX_RECURSIVE:
148+ atts = &globalAttrRecursive;
149+ break;
150+ default:
151+ return ENOTSUP;
152+ }
153+ return pthread_mutex_init(m, atts);
154+}
155+
156 static int mutexLock(pthread_mutex_t *id)
157 {
158 int status;
159@@ -48,23 +116,8 @@ static int mutexLock(pthread_mutex_t *id)
160 return status;
161 }
162
163-/* Until these can be demonstrated to work leave them undefined*/
164-/* On solaris 8 _POSIX_THREAD_PRIO_INHERIT fails*/
165-#undef _POSIX_THREAD_PROCESS_SHARED
166-#undef _POSIX_THREAD_PRIO_INHERIT
167-
168-/* Two completely different implementations are provided below
169- * If support is available for PTHREAD_MUTEX_RECURSIVE then
170- * only pthread_mutex is used.
171- * If support is not available for PTHREAD_MUTEX_RECURSIVE then
172- * a much more complicated solution is required
173- */
174-
175-
176
177-#if defined(_XOPEN_SOURCE) && (_XOPEN_SOURCE)>=500
178 typedef struct epicsMutexOSD {
179 pthread_mutex_t lock;
180- pthread_mutexattr_t mutexAttr;
181 } epicsMutexOSD;
182
183 epicsMutexOSD * epicsMutexOsdCreate(void) {
184@@ -73,32 +126,12 @@ epicsMutexOSD * epicsMutexOsdCreate(void) {
185
186 pmutex = calloc(1, sizeof(*pmutex));
187 if(!pmutex)
188- goto fail;
189-
190- status = pthread_mutexattr_init(&pmutex->mutexAttr);
191- if (status)
192- goto fail;
193-
194-#if defined(_POSIX_THREAD_PRIO_INHERIT) && _POSIX_THREAD_PRIO_INHERIT > 0
195- status = pthread_mutexattr_setprotocol(&pmutex->mutexAttr,
196- PTHREAD_PRIO_INHERIT);
197- if (errVerbose) checkStatus(status, "pthread_mutexattr_setprotocal");
198-#endif /*_POSIX_THREAD_PRIO_INHERIT*/
199-
200- status = pthread_mutexattr_settype(&pmutex->mutexAttr,
201- PTHREAD_MUTEX_RECURSIVE);
202- checkStatus(status, "pthread_mutexattr_settype");
203- if (status)
204- goto fail;
205+ return NULL;
206
207- status = pthread_mutex_init(&pmutex->lock, &pmutex->mutexAttr);
208- if (status)
209- goto dattr;
210- return pmutex;
211+ status = osdPosixMutexInit(&pmutex->lock, PTHREAD_MUTEX_RECURSIVE);
212+ if (!status)
213+ return pmutex;
214
215-dattr:
216- pthread_mutexattr_destroy(&pmutex->mutexAttr);
217-fail:
218 free(pmutex);
219 return NULL;
220 }
221@@ -109,11 +142,9 @@ void epicsMutexOsdDestroy(struct epicsMutexOSD * pmutex)
222
223 status = pthread_mutex_destroy(&pmutex->lock);
224 checkStatus(status, "pthread_mutex_destroy");
225- status = pthread_mutexattr_destroy(&pmutex->mutexAttr);
226- checkStatus(status, "pthread_mutexattr_destroy");
227 free(pmutex);
228 }
229-
230
231+
232 void epicsMutexOsdUnlock(struct epicsMutexOSD * pmutex)
233 {
234 int status;
235@@ -157,178 +188,3 @@ void epicsMutexOsdShow(struct epicsMutexOSD * pmutex, unsigned int level)
236 */
237 printf(" pthread_mutex_t* uaddr=%p\n", &pmutex->lock);
238 }
239-
240
241-#else /*defined(_XOPEN_SOURCE) && (_XOPEN_SOURCE)>=500 */
242-
243-typedef struct epicsMutexOSD {
244- pthread_mutex_t lock;
245- pthread_mutexattr_t mutexAttr;
246- pthread_cond_t waitToBeOwner;
247-#if defined(_POSIX_THREAD_PROCESS_SHARED) && _POSIX_THREAD_PROCESS_SHARED > 0
248- pthread_condattr_t condAttr;
249-#endif /*_POSIX_THREAD_PROCESS_SHARED*/
250- int count;
251- int owned; /* TRUE | FALSE */
252- pthread_t ownerTid;
253-} epicsMutexOSD;
254-
255-epicsMutexOSD * epicsMutexOsdCreate(void) {
256- epicsMutexOSD *pmutex;
257- int status;
258-
259- pmutex = calloc(1, sizeof(*pmutex));
260- if(!pmutex)
261- return NULL;
262-
263- status = pthread_mutexattr_init(&pmutex->mutexAttr);
264- if(status)
265- goto fail;
266-
267-#if defined(_POSIX_THREAD_PRIO_INHERIT) && _POSIX_THREAD_PRIO_INHERIT > 0
268- status = pthread_mutexattr_setprotocol(
269- &pmutex->mutexAttr,PTHREAD_PRIO_INHERIT);
270- if (errVerbose) checkStatus(status, "pthread_mutexattr_setprotocal");
271-#endif /*_POSIX_THREAD_PRIO_INHERIT*/
272-
273- status = pthread_mutex_init(&pmutex->lock, &pmutex->mutexAttr);
274- if(status)
275- goto dattr;
276-
277-#if defined(_POSIX_THREAD_PROCESS_SHARED) && _POSIX_THREAD_PROCESS_SHARED > 0
278- status = pthread_condattr_init(&pmutex->condAttr);
279- checkStatus(status, "pthread_condattr_init");
280- status = pthread_condattr_setpshared(&pmutex->condAttr,
281- PTHREAD_PROCESS_PRIVATE);
282- checkStatus(status, "pthread_condattr_setpshared");
283- status = pthread_cond_init(&pmutex->waitToBeOwner, &pmutex->condAttr);
284-#else
285- status = pthread_cond_init(&pmutex->waitToBeOwner, 0);
286-#endif /*_POSIX_THREAD_PROCESS_SHARED*/
287- if(status)
288- goto dmutex;
289-
290- return pmutex;
291-
292-dmutex:
293- pthread_mutex_destroy(&pmutex->lock);
294-dattr:
295- pthread_mutexattr_destroy(&pmutex->mutexAttr);
296-fail:
297- free(pmutex);
298- return NULL;
299-}
300-
301-void epicsMutexOsdDestroy(struct epicsMutexOSD * pmutex)
302-{
303- int status;
304-
305- status = pthread_cond_destroy(&pmutex->waitToBeOwner);
306- checkStatus(status, "pthread_cond_destroy");
307-#if defined(_POSIX_THREAD_PROCESS_SHARED) && _POSIX_THREAD_PROCESS_SHARED > 0
308- status = pthread_condattr_destroy(&pmutex->condAttr);
309-#endif /*_POSIX_THREAD_PROCESS_SHARED*/
310- status = pthread_mutex_destroy(&pmutex->lock);
311- checkStatus(status, "pthread_mutex_destroy");
312- status = pthread_mutexattr_destroy(&pmutex->mutexAttr);
313- checkStatus(status, "pthread_mutexattr_destroy");
314- free(pmutex);
315-}
316-
317
318-void epicsMutexOsdUnlock(struct epicsMutexOSD * pmutex)
319-{
320- int status;
321-
322- status = mutexLock(&pmutex->lock);
323- checkStatus(status, "pthread_mutex_lock epicsMutexOsdUnlock");
324- if(status)
325- return;
326-
327- if ((pmutex->count <= 0) || (pmutex->ownerTid != pthread_self())) {
328- pthread_mutex_unlock(&pmutex->lock);
329- checkStatus(status, "pthread_mutex_unlock epicsMutexOsdUnlock");
330- errlogPrintf("epicsMutexOsdUnlock but caller is not owner\n");
331- cantProceed("epicsMutexOsdUnlock but caller is not owner");
332- return;
333- }
334-
335- pmutex->count--;
336- if (pmutex->count == 0) {
337- pmutex->owned = 0;
338- pmutex->ownerTid = 0;
339- status = pthread_cond_signal(&pmutex->waitToBeOwner);
340- checkStatusQuit(status, "pthread_cond_signal epicsMutexOsdUnlock", "epicsMutexOsdUnlock");
341- }
342-
343- status = pthread_mutex_unlock(&pmutex->lock);
344- checkStatus(status, "pthread_mutex_unlock epicsMutexOsdUnlock");
345-}
346-
347-static int condWait(pthread_cond_t *condId, pthread_mutex_t *mutexId)
348-{
349- int status;
350-
351- while ((status = pthread_cond_wait(condId, mutexId)) == EINTR) {
352- errlogPrintf("pthread_cond_wait returned EINTR. Violates SUSv3\n");
353- }
354- return status;
355-}
356-
357-epicsMutexLockStatus epicsMutexOsdLock(struct epicsMutexOSD * pmutex)
358-{
359- pthread_t tid = pthread_self();
360- int status;
361-
362- if (!pmutex || !tid) return epicsMutexLockError;
363- status = mutexLock(&pmutex->lock);
364- if (status == EINVAL) return epicsMutexLockError;
365- checkStatus(status, "pthread_mutex_lock epicsMutexOsdLock");
366- if(status)
367- return epicsMutexLockError;
368-
369- while (pmutex->owned && !pthread_equal(pmutex->ownerTid, tid))
370- condWait(&pmutex->waitToBeOwner, &pmutex->lock);
371- pmutex->ownerTid = tid;
372- pmutex->owned = 1;
373- pmutex->count++;
374-
375- status = pthread_mutex_unlock(&pmutex->lock);
376- checkStatus(status, "pthread_mutex_unlock epicsMutexOsdLock");
377- if(status)
378- return epicsMutexLockError;
379- return epicsMutexLockOK;
380-}
381-
382-epicsMutexLockStatus epicsMutexOsdTryLock(struct epicsMutexOSD * pmutex)
383-{
384- pthread_t tid = pthread_self();
385- epicsMutexLockStatus result;
386- int status;
387-
388- status = mutexLock(&pmutex->lock);
389- if (status == EINVAL) return epicsMutexLockError;
390- checkStatus(status, "pthread_mutex_lock epicsMutexOsdTryLock");
391- if(status)
392- return epicsMutexLockError;
393-
394- if (!pmutex->owned || pthread_equal(pmutex->ownerTid, tid)) {
395- pmutex->ownerTid = tid;
396- pmutex->owned = 1;
397- pmutex->count++;
398- result = epicsMutexLockOK;
399- } else {
400- result = epicsMutexLockTimeout;
401- }
402-
403- status = pthread_mutex_unlock(&pmutex->lock);
404- checkStatus(status, "pthread_mutex_unlock epicsMutexOsdTryLock");
405- if(status)
406- return epicsMutexLockError;
407- return result;
408-}
409-
410-void epicsMutexOsdShow(struct epicsMutexOSD *pmutex,unsigned int level)
411-{
412- printf("ownerTid %p count %d owned %d\n",
413- (void *)pmutex->ownerTid, pmutex->count, pmutex->owned);
414-}
415-#endif /*defined(_XOPEN_SOURCE) && (_XOPEN_SOURCE)>=500 */
416diff --git a/modules/libcom/src/osi/os/posix/osdPosixMutexPriv.h b/modules/libcom/src/osi/os/posix/osdPosixMutexPriv.h
417new file mode 100644
418index 0000000..2b6846c
419--- /dev/null
420+++ b/modules/libcom/src/osi/os/posix/osdPosixMutexPriv.h
421@@ -0,0 +1,28 @@
422+/*************************************************************************\
423+* Copyright (c) 2002 The University of Chicago, as Operator of Argonne
424+* National Laboratory.
425+* Copyright (c) 2002 The Regents of the University of California, as
426+* Operator of Los Alamos National Laboratory.
427+* SPDX-License-Identifier: EPICS
428+* EPICS Base is distributed subject to a Software License Agreement found
429+* in file LICENSE that is included with this distribution.
430+\*************************************************************************/
431+
432+#ifndef osdPosixMutexPrivh
433+#define osdPosixMutexPrivh
434+
435+#include <pthread.h>
436+
437+#ifdef __cplusplus
438+extern "C" {
439+#endif
440+
441+/* Returns ENOTSUP if requested mutextype is not supported */
442+/* At the moment, only 0 (default non recursive mutex) and PTHREAD_MUTEX_RECURSIVE are supported */
443+int osdPosixMutexInit(pthread_mutex_t *,int mutextype);
444+
445+#ifdef __cplusplus
446+}
447+#endif
448+
449+#endif /* osdPosixMutexPrivh */
450diff --git a/modules/libcom/src/osi/os/posix/osdSpin.c b/modules/libcom/src/osi/os/posix/osdSpin.c
451index 1f8434b..f039e7c 100644
452--- a/modules/libcom/src/osi/os/posix/osdSpin.c
453+++ b/modules/libcom/src/osi/os/posix/osdSpin.c
454@@ -109,6 +109,8 @@ void epicsSpinUnlock(epicsSpinId spin) {
455 * POSIX MUTEX IMPLEMENTATION
456 */
457
458+#include <osdPosixMutexPriv.h>
459+
460 typedef struct epicsSpin {
461 pthread_mutex_t lock;
462 } epicsSpin;
463@@ -121,8 +123,8 @@ epicsSpinId epicsSpinCreate(void) {
464 if (!spin)
465 goto fail;
466
467- status = pthread_mutex_init(&spin->lock, NULL);
468- checkStatus(status, "pthread_mutex_init");
469+ status = osdPosixMutexInit(&spin->lock, 0);
470+ checkStatus(status, "osdPosixMutexInit");
471 if (status)
472 goto fail;
473
474diff --git a/modules/libcom/src/osi/os/posix/osdThread.c b/modules/libcom/src/osi/os/posix/osdThread.c
475index fe375fd..4525588 100644
476--- a/modules/libcom/src/osi/os/posix/osdThread.c
477+++ b/modules/libcom/src/osi/os/posix/osdThread.c
478@@ -32,6 +32,7 @@
479 #include "ellLib.h"
480 #include "epicsEvent.h"
481 #include "epicsMutex.h"
482+#include "osdPosixMutexPriv.h"
483 #include "epicsString.h"
484 #include "epicsThread.h"
485 #include "cantProceed.h"
486@@ -327,10 +328,10 @@ static void once(void)
487 int status;
488
489 pthread_key_create(&getpthreadInfo,0);
490- status = pthread_mutex_init(&onceLock,0);
491- checkStatusOnceQuit(status,"pthread_mutex_init","epicsThreadInit");
492- status = pthread_mutex_init(&listLock,0);
493- checkStatusOnceQuit(status,"pthread_mutex_init","epicsThreadInit");
494+ status = osdPosixMutexInit(&onceLock,0);
495+ checkStatusOnceQuit(status,"osdPosixMutexInit","epicsThreadInit");
496+ status = osdPosixMutexInit(&listLock,0);
497+ checkStatusOnceQuit(status,"osdPosixMutexInit","epicsThreadInit");
498 pcommonAttr = calloc(1,sizeof(commonAttr));
499 if(!pcommonAttr) checkStatusOnceQuit(errno,"calloc","epicsThreadInit");
500 status = pthread_attr_init(&pcommonAttr->attr);
501diff --git a/modules/libcom/test/Makefile b/modules/libcom/test/Makefile
502index 3ad869a..c49ac94 100755
503--- a/modules/libcom/test/Makefile
504+++ b/modules/libcom/test/Makefile
505@@ -333,6 +333,15 @@ TESTS += nonEpicsThreadPriorityTest
506 endif
507 endif
508
509+ifeq ($(POSIX),YES)
510+ifeq ($(USE_POSIX_THREAD_PRIORITY_SCHEDULING),YES)
511+TESTPROD_HOST += epicsMutexPriorityInversionTest
512+epicsMutexPriorityInversionTest_SRCS += epicsMutexPriorityInversionTest.c
513+epicsMutexPriorityInversionTest_SYS_LIBS_Linux += pthread rt
514+TESTSCRIPTS_HOST+=epicsMutexPriorityInversionTest.t
515+endif
516+endif
517+
518 include $(TOP)/configure/RULES
519
520 rtemsTestData.c : $(TESTFILES) $(TOOLS)/epicsMakeMemFs.pl
521diff --git a/modules/libcom/test/epicsMutexPriorityInversionTest.c b/modules/libcom/test/epicsMutexPriorityInversionTest.c
522new file mode 100644
523index 0000000..8d7c6f2
524--- /dev/null
525+++ b/modules/libcom/test/epicsMutexPriorityInversionTest.c
526@@ -0,0 +1,258 @@
527+#ifndef _GNU_SOURCE
528+#define _GNU_SOURCE
529+#endif
530+#include <sched.h>
531+#include <epicsThread.h>
532+#include <epicsEvent.h>
533+#include <epicsMutex.h>
534+#include <epicsUnitTest.h>
535+#include <errlog.h>
536+#include <testMain.h>
537+#include <time.h>
538+#include <envDefs.h>
539+#include <stdio.h>
540+#include <pthread.h>
541+
542+#define THE_CPU 0
543+
544+#define SPIN_SECS 5000000
545+
546+#if __GLIBC__ * 100 + __GLIBC_MINOR__ >= 204
547+#define HAVE_CPU_AFFINITY
548+#else
549+#undef HAVE_CPU_AFFINITY
550+#endif
551+
552+typedef struct ThreadArg_ {
553+ epicsEventId sync;
554+ epicsEventId done;
555+ epicsMutexId mtx;
556+ volatile unsigned long end;
557+ unsigned long lim;
558+ int pri[3];
559+} ThreadArg;
560+
561+static unsigned long getClockUs()
562+{
563+ struct timespec now;
564+ if ( clock_gettime( CLOCK_MONOTONIC, &now ) ) {
565+ testAbort("clock_gettime(CLOCK_MONOTONIC) failed");
566+ }
567+ return ((unsigned long)now.tv_sec)*1000000 + ((unsigned long)now.tv_nsec)/1000;
568+}
569+
570+/*
571+ * Set affinity of executing thread to 'cpu'
572+ */
573+static void setThreadAffinity(int cpu)
574+{
575+#ifdef HAVE_CPU_AFFINITY
576+ cpu_set_t cset;
577+
578+ CPU_ZERO( &cset );
579+ CPU_SET( cpu, &cset );
580+ pthread_setaffinity_np( pthread_self(), sizeof(cset), &cset );
581+#endif
582+}
583+
584+/*
585+ * Ensure only 'cpu' is set in executing thread's affinity mask
586+ */
587+static void checkAffinity(int cpu)
588+{
589+#ifdef HAVE_CPU_AFFINITY
590+ cpu_set_t cset;
591+
592+ if ( pthread_getaffinity_np( pthread_self(), sizeof(cset), &cset) ) {
593+ testDiag("pthread_getaffinity_np FAILED");
594+ return;
595+ }
596+ if ( 1 != CPU_COUNT( &cset ) || ! CPU_ISSET( cpu, &cset ) ) {
597+ testDiag( "Thread has unexpected CPU affinity mask" );
598+ }
599+#endif
600+}
601+
602+
603+/*
604+ * Make sure executing thread uses SCHED_FIFO and
605+ * store its priority a->pri[idx]
606+ *
607+ * RETURNS: 0 if SCHED_FIFO engaged, nonzero otherwise.
608+ */
609+static int checkThreadPri(ThreadArg *a, unsigned idx)
610+{
611+ int pol;
612+ struct sched_param p;
613+
614+ if ( pthread_getschedparam( pthread_self(), &pol, &p ) ) {
615+ testFail("pthread_getschedparam FAILED");
616+ return 1;
617+ }
618+ if ( a && idx < sizeof(a->pri)/sizeof(a->pri[0]) ) {
619+ a->pri[idx] = p.sched_priority;
620+ }
621+ if ( SCHED_FIFO != pol ) {
622+ testDiag( "Thread uses unexpected scheduler" );
623+ }
624+ return (SCHED_FIFO != pol);
625+}
626+
627+
628+/*
629+ * Low-priority thread.
630+ *
631+ * Lock mutex and signal to the medium-priority thread
632+ * that it may proceed.
633+ *
634+ * Since all three (high-, medium- and low-priority threads)
635+ * execute on the same CPU (via affinity) the medium-
636+ * priority thread will then take over the CPU and prevent
637+ * (unless priority-inheritance is enabled, that is)
638+ * the low-priority thread from continueing on to unlock
639+ * the mutex.
640+ */
641+static void loPriThread(void *parm)
642+{
643+ ThreadArg *a = (ThreadArg*)parm;
644+
645+ checkAffinity( THE_CPU );
646+
647+ checkThreadPri( a, 0 );
648+
649+ epicsMutexMustLock( a->mtx );
650+
651+ epicsEventSignal( a->sync );
652+
653+ /* medium-priority thread takes over CPU and spins
654+ * while the high-priority thread waits for the mutex.
655+ * With priority-inheritance enabled this thread's
656+ * priority will be increased so that the medium-
657+ * priority thread can be preempted and we proceed
658+ * to release the mutex.
659+ */
660+
661+ epicsMutexUnlock( a->mtx );
662+}
663+
664+static void hiPriThread(void *parm)
665+{
666+ ThreadArg *a = (ThreadArg*)parm;
667+
668+ /* Try to get the mutex */
669+ epicsMutexMustLock( a->mtx );
670+ /* Record the time when we obtained the mutex */
671+ a->end = getClockUs();
672+ epicsMutexUnlock( a->mtx );
673+ /* Tell the main thread that the test done */
674+ epicsEventSignal( a->done );
675+}
676+
677+static void miPriThread(void *parm)
678+{
679+ ThreadArg *a = (ThreadArg*)parm;
680+
681+ /* Basic checks:
682+ * - affinity must be set to use single CPU for all threads
683+ * - SCHED_FIFO must be in effect
684+ */
685+ checkAffinity( THE_CPU );
686+ checkThreadPri( a, 1 );
687+
688+ /* Create the low-priority thread. */
689+ epicsThreadMustCreate("testLo",
690+ a->pri[0],
691+ epicsThreadGetStackSize( epicsThreadStackMedium ),
692+ loPriThread,
693+ a);
694+
695+ /* Wait until low-priority thread has taken the mutex */
696+ epicsEventMustWait( a->sync );
697+
698+ /* Compute the end-time for our spinning loop */
699+ a->lim = getClockUs() + SPIN_SECS;
700+ a->end = 0;
701+
702+ /* Create the high-priority thread. The hiPri thread will
703+ * block for the mutex and
704+ * if priority-inheritance is available:
705+ * increase the low-priority thread's priority temporarily
706+ * so it can proceed to release the mutex and hand it to
707+ * the high-priority thread.
708+ * if priority-inheritance is not available:
709+ * the high-priority thread will have to wait until we are
710+ * done spinning and the low-priority thread is scheduled
711+ * again.
712+ */
713+ epicsThreadMustCreate("testHi",
714+ a->pri[2],
715+ epicsThreadGetStackSize( epicsThreadStackMedium ),
716+ hiPriThread,
717+ a);
718+
719+ /* Spin for some time; the goal is hogging the CPU and thus preventing
720+ * the low-priority thread from being scheduled.
721+ * If priority-inheritance is enabled then the low-priority thread's
722+ * priority is temporarily increased so that we can be preempted. This
723+ * then causes the high-priority thread to record the 'end' time which
724+ * tells us that we can terminate early...
725+ */
726+ while ( 0 == a->end && getClockUs() < a->lim )
727+ /* spin */;
728+
729+ /* w/o priority-inheritance the low-priority thread may proceed at
730+ * this point and release the mutex to the high-priority thread.
731+ */
732+}
733+
734+
735+#define NUM_TESTS 2
736+
737+MAIN(epicsMutexPriorityInversionTest)
738+{
739+ ThreadArg a;
740+ long hiPriStalledTimeUs;
741+ struct sched_param p_pri;
742+
743+ a.mtx = epicsMutexMustCreate();
744+ a.sync = epicsEventMustCreate( epicsEventEmpty );
745+ a.done = epicsEventMustCreate( epicsEventEmpty );
746+ a.pri[0] = epicsThreadPriorityLow;
747+ a.pri[1] = epicsThreadPriorityMedium;
748+ a.pri[2] = epicsThreadPriorityHigh;
749+
750+ testPlan(NUM_TESTS);
751+
752+ p_pri.sched_priority = sched_get_priority_min( SCHED_FIFO );
753+ if ( pthread_setschedparam(pthread_self(), SCHED_FIFO, &p_pri) ) {
754+ testDiag("SCHED_FIFO not engaged - cannot run this test");
755+ testSkip( NUM_TESTS, "SCHED_FIFO not engaged" );
756+ return testDone();
757+ } else {
758+ testDiag("SCHED_FIFO is in use");
759+ }
760+
761+ setThreadAffinity( THE_CPU );
762+ /* created threads should inherit CPU affinity mask */
763+
764+ epicsThreadMustCreate("testMi",
765+ a.pri[1],
766+ epicsThreadGetStackSize( epicsThreadStackMedium ),
767+ miPriThread,
768+ &a);
769+
770+ epicsEventMustWait( a.done );
771+
772+ testOk1( (a.pri[0] < a.pri[1]) && (a.pri[1] < a.pri[2]) );
773+
774+ hiPriStalledTimeUs = a.end - (a.lim - SPIN_SECS);
775+
776+ testDiag("High-priority thread stalled for %li us\n", hiPriStalledTimeUs);
777+ testOk1( hiPriStalledTimeUs < 1000 );
778+
779+ epicsEventDestroy( a.done );
780+ epicsEventDestroy( a.sync );
781+ epicsMutexDestroy( a.mtx );
782+
783+ return testDone();
784+}

Subscribers

People subscribed via source and target branches