Select Page

TL;DR:

  • If process B is tracing process A using something like ptrace(), then B is the parent of A.  In that case, B is not necessarily the real_parent of A.
  • If B creates A (e.g. using fork()) but terminates before A, then init (PID 1) now becomes both the parent and the real_parent of A – at least on my 5.8.0.
  • When process A terminates, a SIGCHLD is sent to the parent of A.

I am not 100% sure – please leave a comment if I said anything wrong, and let’s learn together!


Okay, okay.  I know that a struct task_struct instance represents a task, or a “schedulable entity”, not a “process”, but I’m still gonna use “process” for the sake of brevity.  No pthread is created in this post!

We all say that “B is the parent of A” if B “created” A using something like fork().  However, in the Linux kernel, there are two “parent” fields in our task descriptor, struct task_struct, as defined in include/linux/sched.h:

	/* Real parent process: */
	struct task_struct __rcu	*real_parent;

	/* Recipient of SIGCHLD, wait4() reports: */
	struct task_struct __rcu	*parent;

What on earth is the difference between them?


Short answer: no difference for “normal processes” – they both point to the same “parent”.  In order to confirm, I wrote a very simple miscellaneous character driver:

#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/module.h>

static ssize_t hello_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	printk("%s: current->pid: %d\n", __func__, task_pid_nr(current));

	rcu_read_lock();
	printk("%s: current->real_parent->pid: %d\n", __func__,
	       task_pid_nr(rcu_dereference(current->real_parent)));
	printk("%s: current->parent->pid: %d\n", __func__,
	       task_pid_nr(rcu_dereference(current->parent)));
	rcu_read_unlock();

	return 0;
}

static const struct file_operations hello_fops = {
	.owner = THIS_MODULE,
	.read = hello_read,
};

static struct miscdevice hello_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "hello",
	.fops = &hello_fops
};

static int __init hello_init(void)
{
	return misc_register(&hello_dev);
}

static void __exit hello_exit(void)
{
	misc_deregister(&hello_dev);
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Peilin Ye");

It is poorly written in many ways: missing SPDX-License-Identifier tag, missing MODULE_DESCRIPTION…the list goes on.

Anyway, it exposes a device node, /dev/hello, and it dumps current->pid, current->real_parent->pid and current->parent->pid into dmesg, if someone (tries to) read it.  The latter two printk()s are RCU-protected – I borrowed some code from task_ppid_nr_ns(), see include/linux/sched.h.

Yeah, and a Makefile:

obj-m += hello.o
KDIR := /lib/modules/$(shell uname -r)/build

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

Install it:

peilin@PWN:~/Desktop/playground$ sudo insmod hello.ko
peilin@PWN:~/Desktop/playground$ lsmod hello | grep hello
hello
16384 0
peilin@PWN:~/Desktop/playground$ file /dev/hello
/dev/hello: character special (10/56)
peilin@PWN:~/Desktop/playground$ sudo chmod 0444 /dev/hello

Now we cat /dev/hello, and take a look at dmesg:

peilin@PWN:~/Desktop/playground$ cat /dev/hello
peilin@PWN:~/Desktop/playground$ dmesg | tail -3
[334231.298794] hello_read: current->pid: 449498
[334231.298800] hello_read: current->real_parent->pid: 365698
[334231.298800] hello_read: current->parent->pid: 365698
peilin@PWN:~/Desktop/playground$
echo $$
365698

Nice!  It worked.  449498 seems to be the PID of cat.  As you can see, here both real_parent and parent points to my Bash shell, so there’s no difference between them.


Now, now. What if the fork() parent terminates before the child? In order to figure out, I wrote another (userspace) program:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
	if (fork() == 0) {
		int ret = 0;

		/* make sure that our parent terminates before us */
		sleep(3);

		int fd = open("/dev/hello", O_RDONLY);
		if (fd < 0) {
			fprintf(stderr, "%d: failed to open /dev/hello\n", getpid());
			exit(-1);
		}
	
		if (read(fd, NULL, 0) < 0) {
			fprintf(stderr, "%d: failed to read /dev/hello\n", getpid());
			ret = -1;
		}

		close(fd);
	
		return ret;
	}
	return 0;
}

By the way, it’s quite surprising to me that read(fd, NULL, 0); actually works…

Anyway, run it, wait 3 seconds, and take another look at dmesg:

peilin@PWN:~/Desktop/playground$ gcc -o demo demo.c
peilin@PWN:~/Desktop/playground$
./demo
peilin@PWN:~/Desktop/playground$
sleep 3
peilin@PWN:~/Desktop/playground$
dmesg | tail -3
[335409.670896] hello_read: current->pid: 453660
[335409.670899] hello_read: current->real_parent->pid: 1
[335409.670899] hello_read: current->parent->pid: 1

Interesting!  Since the parent has already terminated when the child issues read(), both real_parent and parent now point to init, “the parent of all processes”.


Finally, if process B traces process A using ptrace(), B becomes the parent of A. B is not necessarily the real_parent of A.

I modified my userspace demo a little bit to make the child process loop forever, so I can get a chance to attach GDB to it. GDB uses ptrace() to debug processes. Note, in order to attach GDB to a process (i.e. gdb -p <PID>), you may have to switch to the root user, or do echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope if you “don’t care about security” 🙂

Anyway, after attaching GDB to the child process and resuming it using the c (“continue”) command, let’s take one last look at dmesg:

peilin@PWN:~/Desktop/playground$ pidof gdb
459290

peilin@PWN:~/Desktop/playground$ dmesg | tail -3
[337238.115535] hello_read: current->pid: 459265
[337238.115537] hello_read: current->real_parent->pid: 1
[337238.115538] hello_read: current->parent->pid: 459290

Wonderful! Three different PIDs! The child process reading /dev/hello is 459265; its real_parent now becomes init (1) since its “original parent” who created it has already terminated; finally, its parent is now GDB (459290).

As the parent of the child, GDB will receive a SIGCHLD when the child exits.

…and that’s it! I hope you enjoyed it.