Fundamentals 17 min read

Deep Comparison of Linux Processes and Threads from the Kernel Implementation Perspective

This article explains how Linux implements processes and threads, showing that both are represented by the same task_struct structure, detailing thread creation via pthread_create and clone, and comparing the kernel‑level steps—including flag handling, copy_process, and resource sharing—that differentiate threads from full processes.

Refining Core Development Skills
Refining Core Development Skills
Refining Core Development Skills
Deep Comparison of Linux Processes and Threads from the Kernel Implementation Perspective

Hello everyone, I'm Feige!

In Linux, processes and threads are core concepts, yet many people still confuse their relationship and differences.

Most discussions focus on their differences, but in Linux the similarities far outweigh the differences; threads are even called lightweight processes.

Below is a deep comparison of processes and threads from the Linux kernel implementation viewpoint.

1. Thread Creation Methods

Redis 6.0+ supports multithreading for core services; we use it as an example.

After the Redis main thread starts, it calls initThreadedIO to create multiple I/O threads.

//file:src/networking.c
void initThreadedIO(void) {
    // Start I/O thread creation
    for (int i = 0; i < server.io_threads_num; i++) {
        pthread_t tid;
        pthread_create(&tid, NULL, IOThreadMain, (void*)(long)i);
        io_threads[i] = tid;
    }
}

The thread creation uses pthread_create , which in glibc ultimately calls __pthread_create_2_1 → create_thread . The create_thread function sets various flag bits for the new thread.

//file:nptl/sysdeps/pthread/createthread.c
static int create_thread(struct pthread *pd, ...)
{
    int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL
        | CLONE_SETTLS | CLONE_PARENT_SETTID
        | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
        | 0);
    int res = do_clone(pd, attr, clone_flags, start_thread,
        STACK_VARIABLES_ARGS, 1);
    ...
}

The flags passed to do_clone are crucial; they include CLONE_VM , CLONE_FS , CLONE_FILES , etc. do_clone finally invokes the clone system call, which jumps into kernel code.

//file:sysdeps/unix/sysv/linux/i386/clone.S
ENTRY (__clone)
    ...
    movl $SYS_ify(clone), %eax
    ...

2. Kernel Representation of Threads

Before diving into creation, let’s look at the kernel data structure that represents a thread.

Both processes and threads are abstracted as a task_struct in the source code.

The definition resides in include/linux/sched.h :

//file:include/linux/sched.h
struct task_struct {
    // 1.1 task state
    volatile long state;
    // 1.2 pid and tgid
    pid_t pid;
    pid_t tgid;
    // 1.3 task tree relationships
    struct task_struct __rcu *parent;
    struct list_head children;
    struct list_head sibling;
    struct task_struct *group_leader;
    // 1.4 scheduling priority
    int prio, static_prio, normal_prio;
    unsigned int rt_priority;
    // 1.5 address space
    struct mm_struct *mm, *active_mm;
    // 1.6 filesystem info
    struct fs_struct *fs;
    // 1.7 open files info
    struct files_struct *files;
    // 1.8 namespaces
    struct nsproxy *nsproxy;
    ...
};

All fields are the same for threads and processes because they share the same structure.

Two important fields are pid and tgid . pid uniquely identifies each task_struct . For a process, pid is the process ID. For threads, each thread gets a distinct pid , while tgid records the ID of the owning process.

3. Thread Creation Process

To understand the exact differences, we compare thread creation with process creation.

3.1 Review of Process Creation

Process creation uses the fork system call, which ultimately calls do_fork with flags SIGCHLD, 0, 0, NULL, NULL .

//file:kernel/fork.c
SYSCALL_DEFINE0(fork)
{
    return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}

do_fork then calls copy_process to create the new task.

//file:kernel/fork.c
long do_fork(...)
{
    struct task_struct *p;
    p = copy_process(clone_flags, ...);
    ...
}

3.2 Thread Creation

Thread creation starts from pthread_create , which eventually invokes clone with a set of flags.

//file:nptl/sysdeps/pthread/createthread.c
static int create_thread(struct pthread *pd, ...)
{
    int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL
        | CLONE_SETTLS | CLONE_PARENT_SETTID
        | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM | 0);
    int res = do_clone(pd, attr, clone_flags, ...);
    ...
}

The clone syscall is defined as:

//file:kernel/fork.c
SYSCALL_DEFINE5(clone, ...)
{
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}

Just like fork , do_fork ends up calling copy_process .

3.3 Differences Between Process and Thread Creation

The only real difference lies in the clone_flags passed to do_fork :

Process creation flag: only SIGCHLD

Thread creation flags: CLONE_VM , CLONE_FS , CLONE_FILES , CLONE_SIGNAL , CLONE_SETTLS , CLONE_PARENT_SETTID , CLONE_CHILD_CLEARTID , CLONE_SYSVSEM

Key flags:

CLONE_VM : share address space

CLONE_FS : share filesystem information

CLONE_FILES : share open file descriptor table

4. Inside do_fork

The core of do_fork is copy_process , which copies the parent task_struct and then duplicates or shares various resources based on the flags.

//file:kernel/fork.c
static struct task_struct *copy_process(...)
{
    // 4.1 duplicate task_struct
    struct task_struct *p = dup_task_struct(current);
    ...
    // 4.2 copy files_struct
    retval = copy_files(clone_flags, p);
    // 4.3 copy fs_struct
    retval = copy_fs(clone_flags, p);
    // 4.4 copy mm_struct
    retval = copy_mm(clone_flags, p);
    // 4.5 copy namespaces
    retval = copy_namespaces(clone_flags, p);
    // 4.6 allocate pid and set tgid
    pid = alloc_pid(p->nsproxy->pid_ns);
    p->pid = pid_nr(pid);
    p->tgid = p->pid;
    if (clone_flags & CLONE_THREAD)
        p->tgid = current->tgid;
    ...
}

4.1 Duplicating task_struct

dup_task_struct allocates a new kernel object and copies the original structure (without deep‑copying sub‑objects).

//file:kernel/fork.c
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    tsk = alloc_task_struct_node(node);
    err = arch_dup_task_struct(tsk, orig);
    ...
}

4.2 Copying Open File List ( files_struct )

If CLONE_FILES is set (thread creation), the existing files_struct is shared and its reference count is incremented.

//file:kernel/fork.c
static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
{
    struct files_struct *oldf = current->files, *newf;
    if (clone_flags & CLONE_FILES) {
        atomic_inc(&oldf->count);
        goto out;
    }
    newf = dup_fd(oldf, &error);
    tsk->files = newf;
    ...
}

4.3 Copying Filesystem Information ( fs_struct )

With CLONE_FS , the same fs_struct is shared; otherwise a new one is allocated.

//file:kernel/fork.c
static int copy_fs(unsigned long clone_flags, struct task_struct *tsk)
{
    struct fs_struct *fs = current->fs;
    if (clone_flags & CLONE_FS) {
        fs->users++;
        return 0;
    }
    tsk->fs = copy_fs_struct(fs);
    return 0;
}

4.4 Copying Memory Address Space ( mm_struct )

When CLONE_VM is present (thread), the same mm_struct is shared; otherwise a duplicate is created.

//file:kernel/fork.c
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
    struct mm_struct *oldmm = current->mm, *mm;
    if (clone_flags & CLONE_VM) {
        atomic_inc(&oldmm->mm_users);
        mm = oldmm;
        goto good_mm;
    }
    mm = dup_mm(tsk);
good_mm:
    return 0;
}

Thus all threads of a process share the same address space, filesystem info, and open file table, making them “lightweight”.

5. Conclusion

The entire thread creation flow shows that a thread shares mm_struct , fs_struct , and files_struct with its creator, while a process gets independent copies of each.

In contrast, a process must allocate and initialize its own address space, mount points, and file descriptor tables.

From the kernel’s perspective, user‑space threads are essentially processes that share most resources, hence the term “lightweight processes”.

Performance tests show context‑switch overheads of ~2.7‑5.48 µs for processes and ~3.8 µs for threads, indicating only a modest difference.

kernelLinuxThreadprocessforkclonedo_forktask_struct
Refining Core Development Skills
Written by

Refining Core Development Skills

Fei has over 10 years of development experience at Tencent and Sogou. Through this account, he shares his deep insights on performance.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.