Linux Watchdog Start Stop Ping
Controlling the /dev/watchdog character device from user space: opening, stopping safely with magic close, and sending keep-alive pings
Every watchdog driver we built earlier in this free linux kernel development course exposes itself to user space as a character device, usually /dev/watchdog. Kernel-side registration is only half the story — a real embedded product needs a user-space daemon or application that actually talks to that device correctly, or the watchdog becomes a liability instead of a safety net. This lecture is entirely about that user-space contract: how opening the device arms it, why closing the file is not enough to disarm it, and how to keep the system alive with keep-alive pings. If you are building a free embedded linux course style watchdog client, this is the lecture that gets it right.
What You Will Learn
Prerequisites
Opening the Device Arms It
Unlike most character devices, opening /dev/watchdog is not a passive act. The moment the open call succeeds, the kernel’s watchdog core treats the countdown as active, and if nobody services it before the timeout elapses, the platform’s restart handler fires. This is deliberate: a watchdog that has to be explicitly “started” after open is a watchdog that can be forgotten. Your application should open the device as early as possible in its boot sequence, right after any critical initialization, and never delay it waiting on non-essential subsystems.
A well-behaved opener always checks the return value and distinguishes the failure modes, since each one calls for a different corrective action rather than a generic error message:
|
+– success –> watchdog is now counting down
|
+– ENOENT –> no watchdog driver bound / module not loaded
|
+– EACCES –> insufficient permission, needs root or device group
|
+– EBUSY –> another process already holds the device open
int wdfd = open("/dev/watchdog0", O_WRONLY);
if (wdfd == -1) {
switch (errno) {
case ENOENT:
fprintf(stderr, "ep_wdt_ctl: no watchdog bound at this node\n");
break;
case EACCES:
fprintf(stderr, "ep_wdt_ctl: run as root or add udev rule\n");
break;
case EBUSY:
fprintf(stderr, "ep_wdt_ctl: device already held open elsewhere\n");
break;
default:
fprintf(stderr, "ep_wdt_ctl: open failed: %s\n", strerror(errno));
}
exit(EXIT_FAILURE);
}
Why close() Does Not Stop the Watchdog
This trips up almost every developer the first time they touch a watchdog device: calling close(fd) on its own does not disarm the countdown. The driver has no way to know whether your process is exiting cleanly or has crashed, and a crashed supervisor is exactly the situation a watchdog exists to catch. So the default assumption on close is “the supervisor is gone, keep counting” — and on most drivers that means a reset is still coming.
To stop the watchdog on purpose, you first write a single magic byte — the ASCII character V — into the device file before closing it. This is called the magic close feature. Writing V tells the driver “the next close is intentional, disarm on it.” Any other character written to the device is treated as an ordinary keep-alive ping instead, which is why the kernel documentation is explicit that you should never write a stray V as part of a longer status string — doing so would accidentally disarm the watchdog.
const char magic = 'V';
ssize_t n = write(wdfd, &magic, 1);
if (n != 1) {
fprintf(stderr, "ep_wdt_ctl: magic close write failed: %s\n",
strerror(errno));
} else {
close(wdfd);
printf("ep_wdt_ctl: watchdog disarmed cleanly\n");
}
There is one important exception. If the driver was built or configured with CONFIG_WATCHDOG_NOWAYOUT, magic close is disabled entirely at the kernel level — once armed, the watchdog cannot be stopped by any user-space action for the lifetime of the boot. This is a common production hardening choice: it guarantees that a compromised or buggy supervisor process can never silently disable the safety net. If you rely on this option, your driver must still advertise the WDIOF_MAGICCLOSE flag correctly so tooling can detect the behaviour, even though the write itself becomes a no-op.
Sending Keep-Alive Pings
Between opening and (optionally) closing, your supervisor’s real job is to prove liveness on a schedule. The watchdog framework gives you two equivalent ways to do this, and most production daemons use one or the other consistently rather than mixing them.
Method 1: Write Any Character
A plain write() of one byte to the device — any byte other than V — is defined as a keep-alive ping. This is the simplest possible mechanism and works even from a shell script.
const char ping = '1';
write(wdfd, &ping, 1);
Method 2: WDIOC_KEEPALIVE ioctl
The alternative is an explicit ioctl call, which some teams prefer because it cannot be confused with a data write and reads clearly in code review. The argument is ignored by the kernel, so passing 0 is conventional.
if (ioctl(wdfd, WDIOC_KEEPALIVE, 0) == -1) {
fprintf(stderr, "ep_wdt_ctl: keepalive ioctl failed: %s\n",
strerror(errno));
}
Either mechanism only works if the driver advertised the WDIOF_KEEPALIVEPING capability flag — you will learn how to query that flag safely in the next lecture. As a rule of thumb, feed the watchdog at roughly half its configured timeout: a 30-second timeout should see a ping at least every 15 seconds, giving one full missed cycle of margin before the deadline is at real risk.
Building ep_wdt_ctl: A Minimal Watchdog Supervisor
Below is a small original command-line tool that ties the lecture together: it opens the device, pings it in a loop at half the requested interval, and disarms it cleanly with magic close on SIGINT.
// ep_wdt_ctl.c — minimal watchdog supervisor demo
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <linux/watchdog.h>
static int wdfd = -1;
static volatile sig_atomic_t stop_requested;
static void on_sigint(int sig) { (void)sig; stop_requested = 1; }
int main(int argc, char *argv[])
{
const char *node = (argc > 1) ? argv[1] : "/dev/watchdog0";
int ping_interval_s = (argc > 2) ? atoi(argv[2]) : 15;
signal(SIGINT, on_sigint);
wdfd = open(node, O_WRONLY);
if (wdfd == -1) {
fprintf(stderr, "ep_wdt_ctl: open(%s) failed: %s\n",
node, strerror(errno));
return EXIT_FAILURE;
}
printf("ep_wdt_ctl: armed on %s, pinging every %ds\n",
node, ping_interval_s);
while (!stop_requested) {
if (ioctl(wdfd, WDIOC_KEEPALIVE, 0) == -1)
fprintf(stderr, "ep_wdt_ctl: ping failed: %s\n", strerror(errno));
else
printf("ep_wdt_ctl: keep-alive sent\n");
sleep(ping_interval_s);
}
const char magic = 'V';
write(wdfd, &magic, 1);
close(wdfd);
printf("ep_wdt_ctl: disarmed and exiting cleanly\n");
return EXIT_SUCCESS;
}
$ sudo ./ep_wdt_ctl /dev/watchdog0 5
ep_wdt_ctl: armed on /dev/watchdog0, pinging every 5s
ep_wdt_ctl: keep-alive sent
ep_wdt_ctl: keep-alive sent
^C
ep_wdt_ctl: disarmed and exiting cleanly
Common Mistakes and Troubleshooting
Assuming close() disarms the watchdog
Always send the magic V write before closing, or the next timeout will still reset the board.
Writing ‘V’ inside a longer status string
Any buffer written to the device that happens to contain a V byte can trigger an unintended disarm — keep ping payloads and magic-close writes strictly separate.
Pinging too close to the deadline
A supervisor that pings only just before timeout leaves no margin for scheduling jitter under load. Feed at roughly half the timeout, not at 90% of it.
Expecting magic close to work under NOWAYOUT
If the driver was built with CONFIG_WATCHDOG_NOWAYOUT, the magic write is silently ineffective — design your supervisor to run for the entire uptime of the system in that configuration.
Best Practices
Summary and Key Takeaways
Opening /dev/watchdog arms the countdown immediately, and closing the file alone does not stop it — you must write the magic V character first, unless CONFIG_WATCHDOG_NOWAYOUT makes the watchdog permanently unstoppable. Keeping the system alive is a matter of pinging on a schedule, either with a plain write or the clearer WDIOC_KEEPALIVE ioctl, at roughly half the configured timeout. With this lifecycle understood, the next lecture moves on to querying watchdog capabilities and controlling the timeout and pretimeout values themselves.
Frequently Asked Questions
Does opening /dev/watchdog start the timer immediately?
Yes. The kernel’s watchdog core begins counting down as soon as the open() call succeeds, so the ping loop should start right after.
What happens if I just call close() without writing ‘V’ first?
On most drivers the watchdog keeps counting after close, and if nothing else feeds it, the system resets once the timeout elapses.
Can I write any character to send a keep-alive ping?
Any byte works except ‘V’, which is reserved as the magic close character and will attempt to disarm the watchdog instead.
Why would a driver refuse to be stopped at all?
If it was built with CONFIG_WATCHDOG_NOWAYOUT, the magic close feature is disabled at the kernel level for the entire boot session.
Is WDIOC_KEEPALIVE better than a raw write?
Functionally they are equivalent when the driver advertises WDIOF_KEEPALIVEPING; the ioctl is preferred mainly for code clarity.
How often should I feed the watchdog?
A common rule of thumb is half the configured timeout, giving one full missed cycle of margin before a real risk of reset.
What does EBUSY on open mean for a watchdog device?
It usually means another process already holds the device open — only one supervisor should own the watchdog file descriptor at a time.
Continue the Free Linux Kernel Development Course
Next: querying watchdog capabilities and controlling timeout/pretimeout values.
Next Lecture Back to Course Index