From the 408 OS Exam to Production: Why Is the Signal-Sending Function Called kill Instead of send?

Tech Sharing1676 words

AI translation notice: This English version was translated from the original Chinese article with AI assistance. Some nuances may differ from the original.

Recently, while reviewing the “Process” chapter of the 408 operating systems course, I got to the signal mechanism of inter-process communication (IPC). Since this round was the intensive review phase, I was reading the textbook very carefully.

I froze for a moment: “Wait—sending a signal between processes, and the system call is called kill()?”

That cannot be right. Is all inter-process communication meant to kill the other party? To use an analogy: you send a friend a message saying “come downstairs and hang out,” and someone calls that operation execute_friend(best_buddy). That is absurd, and genuinely counterintuitive, so I dug in a little deeper to find out what was going on.

Why did the UNIX forefathers choose such a misleading name? Today let us dig into this “legacy naming” that has stayed with us for decades, and take a look at what it actually means in everyday development and in interviews.


The “Force Kill” the Name Misleads You Into

When I first saw the kill() system call, or the kill command in a terminal, I formed an obvious assumption: its only job is to “take out a process.”

When troubleshooting a production incident or debugging a program, we habitually type kill <pid>, or the more brutal kill -9 <pid>, into a terminal. As the process vanishes instantly, I naturally equated that operation with the way a program ends.

But in fact, that intuition of mine concealed a major technical misunderstanding.

The essence of kill() is to deliver a signal to a specified process, not to “kill” it in the literal sense. Terminating the process is merely the default behavior of a process on receiving a particular signal, not a functional limit of the system call itself. In other words, kill is nothing more than an asynchronous communication mechanism for notifications, and “killing” is just one default result among many ways of handling signals.

What Does kill(pid, sig) Actually Do?

Let us first look at how kill() is normally used.

In C, the prototype of the kill() system call is very simple:

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
  • pid: the ID card number of the target process.
  • sig: the number of the signal you want to send (such as SIGTERM, SIGUSR1, and so on). If you pass 0 for sig (known as the null signal), kill() does not send a signal at all; it is used to probe whether the process exists and whether the current user has permission to send it signals (commonly used for liveness checks when writing daemons).

A Simple C Example

The code below shows how to send a custom signal (SIGUSR1) to a specified process, and how to check the process's status:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char *argv[]){
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <pid>\n", argv[0]);
        exit(1);
    }

    pid_t target_pid = atoi(argv[1]);

    // 1. 探测进程是否存在
    if (kill(target_pid, 0) == 0) {
        printf("进程 %d 存活,正在发送自定义信号 SIGUSR1...\n", target_pid);
        // 2. 发送 SIGUSR1 信号
        kill(target_pid, SIGUSR1);
    } else {
        perror("进程不存在或无权限");
    }

    return 0;
}

Here, kill() merely delivers the signal into the kernel's process control block (PCB), completing a lightweight event notification.

The Historical Puzzle: Why kill and Not send?

Since its essence is sending a signal, why is it not called send_signal(), or simply send()? That would seem more intuitive.

Answering that means rewinding to the wild early days of UNIX in the 1970s.

  1. Historical constraints and original intent: In the earliest UNIX designs, inter-process communication was nowhere near as complex as it is today. There were very few kinds of signals, and the one purpose, the core scenario, they were designed for was—terminating processes that had run out of control, were stuck in an infinite loop, or were hogging resources. So the intuitive mission of this system call at the time was “take it out,” and the name kill was extremely apt.
  2. Path dependence in the POSIX standard: As systems evolved, people found this “asynchronous notification mechanism” too useful to confine to killing processes; it could also announce state changes, reload configuration, report that a child process had exited, and more. Signals multiplied, and the feature expanded into general-purpose IPC. But by then the name kill() had already taken root in countless system source files and early books. To maintain backward compatibility, the POSIX standard ultimately kept the name.
  3. Distinction from data-transfer APIs: Names like send() and sendto() usually accompany the transfer of large byte streams in network programming and pipes. Traditional UNIX signals, by contrast, are extremely lightweight: they carry no extra data payload (modern sigqueue can pass a small amount of data, but that is an extension), and convey only a pure event notification that “something happened.” So avoiding send also avoids confusion with data transfer.

Common Signals at a Glance

During development and in interviews, a handful of high-frequency signals are worth knowing. Here I had AI summarize them for me, which also makes them handy for later use:

Signal Name Default Number Default Behavior Typical Use Case / Meaning
SIGHUP 1 Terminates the process Sent when the terminal disconnects; modern servers often repurpose it as Nginx/daemon “reload configuration”
SIGINT 2 Terminates the process Ctrl + C, interrupts the foreground process
SIGQUIT 3 Core dump and terminate Ctrl + \ (backslash), which not only quits but leaves a core dump file for debugging
SIGKILL 9 Forcibly kills the process Cannot be caught or ignored. The kernel reclaims it directly; the program does not even get a chance to say goodbye
SIGSEGV 11 Core dump and terminate Segmentation fault (illegal memory access, such as a wild pointer or out-of-bounds access)
SIGPIPE 13 Terminates the process Triggered by continuing to write to a pipe/socket with no read end
SIGALRM 14 Terminates the process Timer expiration (triggered by the alarm() function)
SIGTERM 15 Terminates the process Software termination (what the default kill command sends). Can be caught, and is the first choice for graceful shutdown
SIGSTOP 19 Suspends the process Cannot be caught or ignored. Suspends execution (like pressing pause)
SIGCONT 18 Resumes the process Resumes a process suspended by SIGSTOP or for some other reason
SIGUSR1/2 30/31 Terminates the process User-defined signals; developers can assign them whatever business meaning they like

AI also went ahead and wrote an extra exam question all on its own, so I will record it here too:

💡 A high-frequency interview question:

Why can SIGKILL (9) and SIGSTOP (19) not be caught or ignored?

Answer: Because they are the kernel's “absolute control” reserved for the administrator. If user code were allowed to catch SIGKILL, and someone happened to write an infinite loop that catches it and does nothing, the system could never again kill that rogue process by conventional means, and the whole system would spin completely out of control.

Real-World Use Cases

Where does the kill command actually get used day to day? Again, I had AI give me a quick summary:

1. Daily Operations in the Shell

  • kill <pid>: sends SIGTERM (15) by default. It gives the process a polite hint: “Buddy, that is about enough—wrap up what you are doing and head out.”
  • kill -9 <pid>: sends SIGKILL (9). “No more talk—vanish this instant!” (Note: using 9 all the time easily leads to disasters such as uncommitted database transactions, temp files left behind, and unreleased locks.)
  • kill -HUP <pid>: sends SIGHUP (1). In servers such as Nginx, it is commonly used for hot reloading: on receiving it, the master process reloads its configuration and gracefully restarts the worker processes, with zero service interruption.

2. Backend Development and Graceful Shutdown

When writing web services in Go, Java, or Node.js, if we stop a container with Docker (which sends SIGTERM) or press Ctrl+C in a terminal (which sends SIGINT), the program cannot simply drop dead. A competent backend framework will listen for these termination signals:

  • After receiving SIGTERM, stop accepting new HTTP requests.
  • Finish the requests currently in flight and write them to the database.
  • Disconnect from Redis and MySQL, release resources safely, and then exit gracefully.

3. Simple Inter-Process Communication (IPC)

In some multi-process daemons (such as the Master-Worker pattern), the master process can use kill(worker_pid, SIGUSR1) to tell a child process: “The log file has been rotated, so please reopen the log file handle.”

What Should Ordinary Developers Watch Out For?

Even if you usually write high-level business code and never call the system-level kill() directly, you have definitely hit—or will sooner or later hit—the following signal-related “reefs”:

  1. The power of Ctrl+C: You run a long script in a terminal, your hand slips, and you press Ctrl+C. In essence, the terminal driver has just sent SIGINT to the foreground process group.
  2. Zombie processes and SIGCHLD: A parent process forks a child; if the child dies and the parent never reaps its status through wait(), the child becomes a zombie process. And when the child exits, the kernel sends the parent a SIGCHLD signal. Many high-performance frameworks reap child processes efficiently precisely by catching SIGCHLD asynchronously.
  3. Interrupted system calls (EINTR): If your program is in the middle of a blocking I/O operation (such as a read on a socket), and a signal arrives and its handler runs, that read may be interrupted and return 1, with errno set to EINTR. When writing low-level network libraries, you must explicitly handle this “interrupted by a signal” case (usually by wrapping it in an outer while loop and retrying).
  4. Async-signal safety (Async-Signal-Safe): When writing a signal handler, what you can do inside it is extremely limited. Never call non-thread-safe or non-async-signal-safe functions such as printf() or malloc() inside a signal handler, or you may easily cause deadlocks or memory corruption that are very hard to track down.
  5. Signals do not queue (non-real-time signals get lost): Traditional standard signals (1–31) do not queue. If the same signal is sent to a process several times in quick succession, the process may only perceive it once. If your business logic absolutely cannot drop a single event, standard signals will not do the job (you need real-time signals, or RT signals).

Conclusion

After going through all of this, I finally truly digested the inter-process communication function, and a little extra reading also gave me a deeper understanding of IPC.

But honestly, this kind of misunderstanding is scary. Without AI helping me summarize things and answer my questions, I probably would not have wanted to research on my own why the IPC function is called kill() for quite a while; I might have just assumed that inter-process communication exists entirely to finish the other party off. I also wonder how much more historical debt like this is out there...

I hope this note can also help developers who are grinding through 408 operating systems, or whose scalps are tingling from wrestling with Linux signals, finally sort out the technical truth behind this historical baggage.

Copyright
  • Author:Mark Xu
  • Copyright:All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating otherwise.

© 2026 Mark Xu. All rights reserved.

All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating otherwise.

A small site built with React + Vite + Tailwind