Repository navigation
Conversation
Repeatedly loadrt -> addf -> delf -> unloadrt a component whose function busy-waits 20 us in a 100 us thread that keeps running. DELF_ITER (default 200) sets the number of cycles.
The realtime thread walks its funct_list without the HAL mutex. hal_del_funct_from_thread() and free_funct_struct() (unloadrt) unlinked the entry with list_remove_entry(), which points the entry's links at itself, and returned it to the free list at once. A thread standing on the entry then loops on it or follows a recycled link, and the following unloadrt dlclose()s code the thread may still run: rtapi_app dies or the thread hangs. Unlink the entry keeping its own links, then wait until the thread has completed two more passes (beatcnt, published with a release store) before the entry is freed. If the thread does not complete a pass within 1000 periods the entry is leaked and delf returns -ETIMEDOUT.
|
Interesting use case, just out of curiosity, what are you doing where you need swapping components with the thread running? |
|
2.9 backport, for reference: branch Differences from this PR:
Results, same setup as above (ubuntu:24.04 container, uspace without SCHED_FIFO,
If you'd rather have this in 2.9 as well, I can open a separate PR against 2.9. |
|
@yurc did you see my text? |
|
Hi Luca, sorry for the late reply, and thanks for the review!
We use LinuxCNC not only for machine tools but also as the controller for
automation cells: a rotary table, a conveyor, a palletizing robot, all on
the same engine.
Each cell's logic is an IEC 61131-3 program (SFC/ST) compiled by MATIEC
into a HAL component. Motion goes through PLCopen-style blocks
(MC_MoveAbsolute, MC_MoveVelocity, MC_Halt, MC_SetPosition). Cells talk to
each other through PackML tags over NATS.
The operator can swap a cell's program on a running station. The servo
thread with the EtherCAT CiA402 drives keeps running, the drives stay in OP
and hold position. So we load the new component and delf/unloadrt the old
one while the thread is running. Restarting HAL just to change a program is
not an option for us.
Today's example: a servo rotary table following a hand gesture from a
camera, with the angle coming in as a tag. That is how we hit the rtapi_app
crash.
The shellcheck warning (SC2034, unused loop variable) is fixed in
6af8e3b.
вс, 4 окт. 2026 г. в 11:43, Luca Toniolo ***@***.***>:
… *grandixximo* left a comment (LinuxCNC/linuxcnc#4630)
<#4630 (comment)>
Interesting use case, just out of curiosity, what are you doing where you
need swapping components with the thread running?
Ps:
test.sh fails shellcheck warning
—
Reply to this email directly, view it on GitHub
<#4630?email_source=notifications&email_token=AAU7VNU254YNFSWA25T3ROL5SIEULA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKOJXHAZDCMJYGQ4KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-5978211848>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAU7VNRLMH5L367NARGRGMD5SIEULAVCNFSNUABDKJSXA33TNF2G64TZHMZTMNRSHEYDKO2JONZXKZJ3GU3DSNZVGUZTEMRXUF3AE>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
from no reply, to two in a row, nice ;-) |
|
what's your handover sequence, do you halt motion blocks before delf, or does new comp take over nets first? |
|
Thanks for the approval! We do break-before-make, and only from a stopped node. The new component never takes over nets while the old one is still on them.
The base layer stays up the whole time: EtherCAT/cia402, the |
Leave beatcnt alone and read the thread's threadbeat pin with hal_get_sint(). The pin reference is only valid in the context that created the thread, so it is mapped through the owner's shmem base, as unlink_pin() does. Read threadbeat, then while threads are running delay two periods (one period on later rounds) and stop once threadbeat advanced by two. No timeout and no leak path: stopping the threads ends the wait.
Run halcompile without sudo and drop the sudo restriction. beat-check.sh waits until threadbeat advanced by two instead of comparing two reads 0.1 s apart; a stuck thread is caught by the timeout in test.sh.
|
Thanks for the review. Updated in 61c5143 and 4fd8613:
Results: ubuntu:24.04 container, uspace without SCHED_FIFO,
|
|
@yurc the package-arch failures come from dropping The established pattern for tests that build components is exactly what you had before: plus @BsAtHome so "no sudo" is already true on RIP; |
package-arch installs test components into the system rtlib and needs
root, same as tests/realtime-math, tests/symbols.0/.1 and
tests/rtapi-shmem: ${SUDO} halcompile --install plus a control file
with Restrictions: sudo. runtests only exports SUDO=sudo for system
builds (scripts/runtests.in), so RIP still runs this test without sudo.
Also update delfvictim.comp's dummy pin from bit to bool, matching the
rest of the tree after the bit->bool rename.
|
Restored |
|
No, there should not be sudo, at all. These tests are static tests and do not require the package install test. This is a build test. @grandixximo has put a build guard in place in multiple tests. |
|
@BsAtHome agreed, and I had lost sight of that being the convention for new tests: skip when testing installed packages, no sudo anywhere. The @yurc concretely: keep test.sh as plain #!/bin/sh
# Builds a realtime component with halcompile, which needs the build
# tree. Skip when testing installed packages.
[ -z "$SYSTEM_BUILD" ]RIP jobs run the test, package-arch skips it, nothing ever touches /usr/lib/linuxcnc/modules. |
Follow the convention for new tests: no sudo anywhere. test.sh runs plain 'halcompile --install' (RIP tree), the control file with 'Restrictions: sudo' is dropped, and an executable skip file (as in tests/kins-frames) skips the test when SYSTEM_BUILD is set, so package-arch never touches /usr/lib/linuxcnc/modules.
|
Done in 005e951: plain Checked on this branch (uspace RIP, Ubuntu 24.04, non-root user): |
| beat = (hal_sint_t)(hal_shmem_base + | ||
| ((char *)thread->threadbeat - (char *)comp->shmem_base)); | ||
| start = hal_get_sint(beat); |
There was a problem hiding this comment.
Casting and fiddling with the pin reference is a bad way to do it. You know the name of the thread when you get here. Therefore, you can determine and construct the full pinname. All this code is only called from non-RT (ULAPI), so that is no problem. You can then use halpr_find_pin_by_name() and you need to use SHMPTR on the reference (and need to do signal deref). Easier is to call hal_getref_p() with the name and HAL_QTYPE_PIN limitation to retrieve the actual pin reference.
You should also add a comment why you need to do it that way (because of the context sensitivity of pin addressing).
There was a problem hiding this comment.
Done in d36ff69: the wait now builds <thread>.threadbeat and resolves it with halpr_find_pin_by_name(), then follows the signal or dummysig the way hal_getref_p() does. I didn't call hal_getref_p() directly because it is ULAPI only, and unloadrt reaches this wait from RTAPI through free_funct_struct(). A comment explains this.
There was a problem hiding this comment.
But you are not really allowed to call rtapi_delay from RTAPI. That is problematic.
Unloading while the threads are running is a problem. Normally, a stop command is issued before unloading is done (at program termination). I guess no one has ever considered your use case. This may need some more consideration to see what alternatives there are.
There was a problem hiding this comment.
Agreed. In 2fcb0d4 nothing waits in RTAPI any more:
delf(hal_del_funct_from_thread()from halcmd, ULAPI) waits for the thread to leave the entry.rtapi_delay()is already used to wait in ULAPI inhal_port_wait_readable().- In RTAPI (
free_funct_struct()fromhal_exit(), or a component's exit code callinghal_del_funct_from_thread(), like scope_rt), there is no wait. If the threads are running, the entry is unlinked but not recycled, andRTAPI_MSG_ERRsays to stop the threads or delf the function before unloading. With stopped threads nothing changes.
The use case is replacing a component while the threads keep running: delf first (it waits), then unloadrt (no entries left). The usual stop-then-unload path is unchanged. If you'd rather have halcmd refuse unloadrt (in ULAPI) while the component still has functions in a running thread, I can add that as a separate change.
There was a problem hiding this comment.
The use case is replacing a component while the threads keep running:
delffirst (it waits), thenunloadrt(no entries left). The usual stop-then-unload path is unchanged. If you'd rather have halcmd refuseunloadrt(in ULAPI) while the component still has functions in a running thread, I can add that as a separate change.
Indeed, it is a good idea to have the unload code test whether there are any functions in use from the component being unloaded. If there are, then it should fail.
This must be done in the hal_lib code when trying to remove the component and holding the mutex to prevent a race between test and unload. First test whether functions of that component are in use an then unload the component. That should work and be sufficient.
There was a problem hiding this comment.
Done in 32a602d/0b36e4861. hal_comp_check_unload() in hal_lib takes the mutex and returns -EBUSY (naming each function) when a function of the component is in a thread while the threads run; halcmd unloadrt calls it before unloading and fails. It has to run before the unload request: rtapi_app_exit() is void and rtapi_app dlclose()s the module whatever hal_exit() returns, so hal_exit() itself cannot refuse.
delf still waits: unlinking does not move a thread that is already on the entry, and freeing the entry at once would drop funct->users and pass the unload check while the thread is still inside. If delf times out, the entry is not freed, users stays non-zero, and unloadrt is refused.
The test now checks: unloadrt refused before delf, works after it, and stop/unloadrt/start unchanged; every wait aborts after 10 s. Indentation of the edited lines restored; the threadbeat message prints the error. Merged onto current master: hal/rtapi tests 71/71, hal-delf-live 10/10 (uspace RIP, non-root).
halrmt.cc has its own unloadrt_comp(); I left it as is — happy to add the same check there if you want.
thread->threadbeat is only valid in the context that created the thread, and delf reaches the wait from halcmd. Instead of rebasing the pointer through the owner's shmem_base, find '<thread>.threadbeat' by name and resolve it (signal or dummysig) in the current context, as hal_getref_p() does. hal_getref_p() itself is ULAPI only, while unloadrt reaches the wait from RTAPI through free_funct_struct(). Also indent the added code with spaces only.
Replace the probabilistic loop with a test that holds the thread inside the function: delfvictim spins while its hold pin is set. delftest.py then runs 'delf' and 'unloadrt' from another process and fails if either returns while the thread is still inside, if it fails after the thread is released, or if the thread stops advancing threadbeat. While delf/unloadrt waits it holds the HAL mutex, so the script drives the victim through its own component pins, which need no mutex. threadbeat is read with hal.get_p() in Python (no 32-bit wrap); a stopped thread ends in the timeout. beat-check.sh is removed.
| beat = (hal_sint_t)(hal_shmem_base + | ||
| ((char *)thread->threadbeat - (char *)comp->shmem_base)); | ||
| start = hal_get_sint(beat); |
There was a problem hiding this comment.
But you are not really allowed to call rtapi_delay from RTAPI. That is problematic.
Unloading while the threads are running is a problem. Normally, a stop command is issued before unloading is done (at program termination). I guess no one has ever considered your use case. This may need some more consideration to see what alternatives there are.
rtapi_delay() must not be used to wait in RTAPI code, and unloadrt reaches free_funct_struct() from RTAPI. Only delf (ULAPI) now waits for the thread to leave the unlinked entry, at most 1000 thread periods; on timeout delf fails with -ETIMEDOUT and the entry is not recycled. In RTAPI, an entry removed from a running thread is not recycled and an error asks to stop the threads or delf the function before unloading. The threadbeat pin is looked up with hal_getref_p(), a missing pin is reported as an error.
The test uses hal.set_p()/hal.get_p() on the victim's pins. The victim holds the thread for 500 periods by itself, so nothing has to be written while delf holds the HAL mutex. After delf returns, v.inside must be false. The only time limit is the one in test.sh.
| /* this funct entry points to our funct, unlink */ | ||
| list_remove_entry(list_entry); | ||
| /* and delete it */ | ||
| free_funct_entry_struct(funct_entry); | ||
| /* done */ | ||
| halpr_mutex_release(); | ||
| return 0; | ||
| funct_entry_unlink(list_entry); | ||
| /* and delete it, once the thread can no longer be on it */ | ||
| retval = funct_entry_release(thread, funct_entry); | ||
| /* done */ | ||
| halpr_mutex_release(); | ||
| return retval; |
There was a problem hiding this comment.
What happened to the indenting?
| list_entry = funct_entry_unlink(list_entry); | ||
| /* and delete it, unless the thread may still be on it */ | ||
| funct_entry_release(thread, funct_entry); | ||
| } else { |
There was a problem hiding this comment.
Here too, what happened to the indenting?
| rtapi_print_msg(RTAPI_MSG_ERR, | ||
| "HAL: ERROR: thread_wait_quiescent: pin '%s' cannot be found\n", | ||
| name); | ||
| /* no threadbeat pin, wait two periods */ |
There was a problem hiding this comment.
Dotting i, crossing t and splitting hairs... Since you now use hal_getref_p, you can capture its return value and print it with the message.
int rv = hal_getref_p(&q);
if(0 != rv) {
/* This is a *very* serious error.
We have a thread that has missing interface pins */
rtapi_print_msg(RTAPI_MSG_ERR,
"HAL: ERROR: thread_wait_quiescent: pin '%s' cannot be found, error=%d\n",
name, rv);
...| def wait(cond): | ||
| while not cond(): | ||
| time.sleep(0.001) | ||
|
|
There was a problem hiding this comment.
This probably needs a guard or infinite waits are possible. We expect things to advance, but should plan for the worst and die with an error in that case.
If this runs for more that 10 seconds of wall time, then we are on a machine that is dying or dead. It is better to abort than to block the runtests process.
…g thread hal_comp_check_unload() checks, under the HAL mutex, whether a function of the component is in a thread while the threads run. halcmd unloadrt calls it before unloading and fails with -EBUSY; hal_lib names the functions. The check has to come before the unload: rtapi_app_exit() is void and rtapi_app dlclose()s the module whatever hal_exit() returns. delf still waits for the thread to leave the removed entry: unlinking does not stop a thread that is already on the entry, and the function's user count only drops once the entry is released. Also: the edited lines keep the indentation of the lines they replace, and the missing threadbeat pin message prints the error code. tests/hal-delf-live: unloadrt must fail before delf and work after it; stop, unloadrt, start works as before; every wait aborts after 10 s.
Symptom
delfof a function from a running thread, followed byunloadrtof its component, killsrtapi_app(halcmd:recv_result 1 failed: Connection reset by peeronunloadrt) or leaves the thread spinning.Cause
The realtime thread walks
thread->funct_listwithout the HAL mutex (thread_task()).hal_del_funct_from_thread()andfree_funct_struct()unlink the entry withlist_remove_entry(), which points the removed entry'snext/prevat itself. A thread standing on that entry keeps calling it.unloadrtthendlclose()s the module while the thread may still execute its code.Fix
funct_entry_unlink(): take the entry out of the list but keep the entry's own links, so a thread standing on it continues to the rest of the list.funct_entry_release()frees the unlinked entry. With threads stopped nothing changes. With threads running:delf(ULAPI): waits until<thread>.threadbeatadvanced by two (the pin is looked up by name), at most 1000 thread periods. On timeoutdelffails with-ETIMEDOUT, the entry is not recycled and an error says the function must not be unloaded.unloadrt/hal_exit()(RTAPI): does not wait (nortapi_delay()in RTAPI). The entry is unlinked but not recycled, andRTAPI_MSG_ERRsays to stop the threads ordelfthe function before unloading.So replacing a component while the threads keep running is:
delf(waits), thenunloadrt(the function is no longer in any thread).Test
tests/hal-delf-live: the function of a small test component (delfvictim) keeps the thread inside for 500 periods when its pinholdis set.delftest.pysetshold, waits forinside, runshalcmd delfand checks thatinsideis false oncedelfhas returned; thenunloadrtandt.threadbeatmust keep advancing. Pins are accessed withhal.set_p()/hal.get_p(); no nets, no wall-clock timeouts (onlytimeoutin test.sh).skipwhenSYSTEM_BUILDis set, no sudo.Results
uspace RIP, Ubuntu 24.04, non-root user:
hal_lib.cdelfreturns while the thread is inside the function)fe72abb20+ same test (2.9 variant)synctwin/hal-delf-quiescence-2.9Why
Replacing a HAL component while the servo thread keeps running, without stopping the machine.
2.9
2.9 has the same bug. It has no
threadbeatpin, so a backport needs a pass counter inhal_thread_t(and aHAL_VERbump). A branch with the same behaviour exists: SyncTwin/linuxcncsynctwin/hal-delf-quiescence-2.9(its test useshalcmd getp/setpand the component's own pass counter, since the 2.9 pythonhalmodule has noget_p). I can open it if you want it in 2.9.Limits
init_funct_list(initf) is not touched.