Linux内核中线程的实现方式

摘要

本文通过对Linux内核源码的研究和两个C/C++程序,探讨了 Linux内核对线程的支持。并得出了一个结论:Linux内核的线程实现是货真价实的。因此,Linux上的多线程实现是真正的多线程实现。所谓Linux内核其实并不支持线程的说法是错误的。

1.    前言

关于Linux 内核中线程的实现方式,有一种说法认为:Linux内核其实并不支持线程,因此,Linux上的多线程实现其实是“伪多线程”。

这种说法到底正确吗?Linux内核到底是否支持多线程?本文作者通过对Linux内核源码的研究和一些C/C++程序来回答这一问题。

2.    进程与线程

按照操作系统教科书中的定义。进程与线程有以下特点:

1) 进程是程序的执行。

2) 在一个进程中,可以有一到多个线程。

3) 这些线程共享同一个地址空间。

4) 但是每个线程有自己独立的运行栈。

5) 每个线程可以被操作系统独立地调度。

3.    描述线程的数据结构

根据http://en.wikipedia.org/wiki/Multithreading_(computer_architecture),多线程编程模式兴起于90年代末。因此,当Linus Torvalds1991年实现Linux的第一个版本是,他根本没有考虑对线程的支持。

在早期的Linux版本中,Linux只支持进程,不支持线程。在早期的Linux的版本中,描述进程的数据结构式struct task_struct,这也就是操作系统教科书中所说的PCBProcess Control Block)。

为了支持线程,当代Linux采用的方式是用struct task_struct既描述进程,也描述线程。图1给出了2.6.32.27内核中描述进程/线程关系的数据结构。

1:描述进程/线程关系的数据结构

 

从上图我们可以看出:

1) 每个线程都用一个独立的task_struct来描述。

2) 同一个进程的多个线程通过task_structthread_group指针字段链接成一个双向循环链表。为了清晰起见,上图只是画出了一个方向的链接。

3) 同一个进程的多个线程共享同一个内存地址空间,因为它们task_structmm指针字段都指向了同一个mm_struct结构。

4) 对于每个信号,同一个进程的多个线程共享同一个信号处理程序。因为它们task_structsighand字段都指向了同一个sighand_struct结构。

5) 同一个进程的多个线程共享同一个文件描述表。因此,一个线程打开的文件,对其它线程也是可见的。

 

 

为了验证上面的结论,我们下面通过一个用户态的多线程程序和一个内核态的模块来进行验证。本文的实验环境如下:

1) Cent OS 版本: 6.5

2) Linux内核2.6.32

3) GCC版本:4.4.7

4.    一个用户态的多线程程序

下面是该用户态程序的源码:

#include <stdio.h>

#include <unistd.h>

#include <pthread.h>

#include <signal.h>

 

static void handler(int sig)

{

   printf("CTRL+C captured\n");

}

static void *threadFunc(void *arg)

{

   printf("In threadFunc()\n");

 

   int fd = dup(0);

 

   printf("fd = %d\n", fd);

 

   struct sigaction sa;

   sigemptyset(&sa.sa_mask);

   sa.sa_flags = 0;

   sa.sa_handler = handler;

   if (sigaction(SIGINT, &sa, NULL) == -1)

   {

       printf("Set SIGINT handler error\n");

   }

 

   sleep(60 * 10); // in seconds

 

   return (void *)1;

}

 

int main(void)

{  

   pthread_t t[100];

   void *res;

   int s;

   int thread_count = 2;

   int i;

 

   for (i=0; i<thread_count; i++)

   {

       s = pthread_create(&t[i], NULL, threadFunc, NULL);

 

       if (s != 0)

           printf("pthread_create() call failed. Return value:%d.\n", s);

   }

 

   for (i=0; i<thread_count; i++)

   {

       s = pthread_join(t[i], &res);

       if (s != 0) {

           printf("pthread_join() call failed. Return value: %d\n", s);

       }

       else {

           printf("pthread_join() call suceeded. Thread exit code: %ld\n", (long)res);

       }

 

   }

 

   return 0;

}

 

下面是编译该程序的Makefile

TARGET = pthread_test

SOURCES = main.cpp

CC = g++

FLAGS = -g -Wall

LIBS = -lm -lstdc++ -pthread

 

 

# Objs are all the sources, with .cpp replaced by .o

OBJS := $(SOURCES:.cpp=.o)

all: $(TARGET)

$(TARGET): $(OBJS)

               $(CC) $(CFLAGS) -o $(TARGET) $(OBJS) $(FLAGS) $(LIBS)

 

.cpp.o:

               $(CC) $(FLAGS) $(INCLUDES) -c $<

clean:

               rm -f *.o

               rm -f $(TARGET)

 

该用户态程序在主线程中通过POSIX Thread库创建了两个线程。在每个线程中:

1) 调用dup(0)复制一个新的标准输入的描述符。

2) 设置了SIGINT信号的处理程序。

3) 睡眠一个小时。

5.    一个内核态的模块

该内核态模块的源码:

#include <linux/module.h>   // included for all kernel modules

#include <linux/kernel.h>   // included for KERN_INFO

#include <linux/init.h>     // included for __init and __exit macros

#include <linux/sched.h>

#include <linux/fdtable.h>

 

 

MODULE_LICENSE("GPL");

 

void dump_schedule_info(struct task_struct *task)

{

   printk("sched_class of task id(%d, comm='%s'):0x%x\n", task->pid, task->comm, task->sched_class);

}

 

void dump_file_struct(struct files_struct *files)

{

   int max_fds = files->fdt->max_fds;

   int fd;

 

   printk("files->fdt->max_fds=%d\n", max_fds);

   for (fd=0;fd<max_fds;fd++)

   {

       if (FD_ISSET(fd, files->fdt->open_fds))

       {

           printk("fd %d is open\n", fd);

       } // if

   } // for

}

 

void dump_sighand_struct(struct sighand_struct *sighand)

{

   int i;

 

   for (i=0; i<_NSIG; i++)

   {

      struct k_sigaction *sigaction = &sighand->action[i];

      if (sigaction->sa.sa_handler == SIG_DFL)

      {

          printk("SIG %d: SIG_DFL\n", i);

      }

      else if (sigaction->sa.sa_handler == SIG_IGN)

      {

          printk("SIG %d: SIG_IGN\n", i);

      }

      else

      {

          printk("SIG %d: 0x%x\n", i, sigaction->sa.sa_handler);

      }

 

 

   } // for

}

 

void enum_threads_in_process(struct task_struct *proc)

{

   struct task_struct *t;

 

   printk("Start enumerating threads in process:%d\n", proc->pid);

 

   int count = 0;

 

   t = proc;

 

   do

   {

       printk("Thread id=%d, comm='%s', files=0x%x, mm=0x%x, active_mm=0x%x, sighand=0x%x\n", t->pid, t->comm, t->files, t->mm, t->active_mm, t->sighand);

       dump_schedule_info(t);

       dump_file_struct(t->files);

       dump_sighand_struct(t->sighand);

       count ++;

 

       t = next_thread(t);

   } while (t != proc);

 

   printk("Thread count in proc(pid=%d):%d\n", proc->pid, count);

}

 

void enum_processes()

{

   struct task_struct *proc;

 

   printk("\n\nStart enumerating processes:\n");

 

   int count = 0;

 

   for_each_process(proc)

   {

      printk("pid=%d, comm='%s', files=0x%x\n", proc->pid, proc->comm, proc->files);

      enum_threads_in_process(proc);

      printk("\n");

      count ++;

   } // for_each_process

 

   printk("Process count:%d\n", count);

}

 

int init_module(void)

{

   enum_processes();

   return 0;    // Non-zero return means that the module couldn't be loaded.

}

 

void cleanup_module(void)

{

   printk(KERN_INFO "Cleaning up module.\n");

}

 

下面是编译该模块的Makefile

obj-m += enum_processes.o

all:

               make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:

               make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

 

在内核加载该内核态模块时,该模块:

1) 枚举出系统中的每一个进程。

2) 对于每个进程,枚举出其每一个线程。

3) 对于每个线程,打印其task_structmm指针字段。

4) 对于每个线程,打印其文件描述符表。

5) 对于每个线程,打印其每个信号的处理程序。

6.    实验

6.1  运行用户态程序pthread_test

我们运行pthread_test,然后按CTRL+C

[liqingxu@localhost posix_thread]$ ./pthread_test

In threadFunc()

fd = 3

In threadFunc()

fd = 4

^CCTRL+C captured

 

通过上面的输出,我们可以看到:

1) 线程成功的设置了SIGINT信号的处理程序。

2) 第一个线程的dup(0)调用返回的文件描述符为3

3) 第二个线程的dup(0)调用返回的文件描述符为4

 

 

6.2  安装内核态模块enum_processes.ko

安装下面的步骤,安装内核态模块enum_processes.ko

1) 切换到root用户。

2) 然后运行tail –f /var/log/messages,以显示该模块注册时打印的信息。

3) insmod enum_processes.ko

 

下面是该模块注册时打印的一部分信息。通过下面highlight出的信息,我们可以看出:

1) 每个线程都共享同一个地址空间,因为其task_struct mm字段指向了同样的mm_struct结构。

2) 每个线程都共享同一个信号处理程序。因为它们task_structsighand字段都指向了同一个sighand_struct结构。

3) 每个线程都共享同一个文件描述表。

这验证了我们前面讨论描述线程的数据结构时得到的结论。

Nov 10 03:43:30 localhost kernel: pid=5703, comm='pthread_test', files=0x4f12b200

Nov 10 03:43:30 localhost kernel: Start enumerating threads in process:5703

Nov 10 03:43:30 localhost kernel: Thread id=5703, comm='pthread_test', files=0x4f12b200,mm=0x4f00c180, active_mm=0x4f00c180, sighand=0x6462b540

Nov 10 03:43:30 localhost kernel: sched_class of task id(5703, comm='pthread_test'):0x8160be60

Nov 10 03:43:30 localhost kernel: files->fdt->max_fds=256

Nov 10 03:43:30 localhost kernel: fd 0 is open

Nov 10 03:43:30 localhost kernel: fd 1 is open

Nov 10 03:43:30 localhost kernel: fd 2 is open

Nov 10 03:43:30 localhost kernel: fd 3 is open

Nov 10 03:43:30 localhost kernel: fd 4 is open

Nov 10 03:43:30 localhost kernel: SIG 0: SIG_DFL

Nov 10 03:43:30 localhost kernel: SIG 1: 0x4007e4

Nov 10 03:43:30 localhost kernel: SIG 2: SIG_DFL

......

Nov 10 03:43:30 localhost kernel: Thread id=5704, comm='pthread_test', files=0x4f12b200,mm=0x4f00c180, active_mm=0x4f00c180, sighand=0x6462b540

Nov 10 03:43:30 localhost kernel: sched_class of task id(5704, comm='pthread_test'):0x8160be60

Nov 10 03:43:30 localhost kernel: files->fdt->max_fds=256

Nov 10 03:43:30 localhost kernel: fd 0 is open

Nov 10 03:43:30 localhost kernel: fd 1 is open

Nov 10 03:43:30 localhost kernel: fd 2 is open

Nov 10 03:43:30 localhost kernel: fd 3 is open

Nov 10 03:43:30 localhost kernel: fd 4 is open

Nov 10 03:43:30 localhost kernel: SIG 0: SIG_DFL

Nov 10 03:43:30 localhost kernel: SIG 1: 0x4007e4

Nov 10 03:43:30 localhost kernel: SIG 2: SIG_DFL

......

Nov 10 03:43:30 localhost kernel: Thread id=5705, comm='pthread_test', files=0x4f12b200,mm=0x4f00c180, active_mm=0x4f00c180, sighand=0x6462b540

Nov 10 03:43:30 localhost kernel: sched_class of task id(5705, comm='pthread_test'):0x8160be60

Nov 10 03:43:30 localhost kernel: files->fdt->max_fds=256

Nov 10 03:43:30 localhost kernel: fd 0 is open

Nov 10 03:43:30 localhost kernel: fd 1 is open

Nov 10 03:43:30 localhost kernel: fd 2 is open

Nov 10 03:43:30 localhost kernel: fd 3 is open

Nov 10 03:43:30 localhost kernel: fd 4 is open

Nov 10 03:43:30 localhost kernel: SIG 0: SIG_DFL

Nov 10 03:43:30 localhost kernel: SIG 1: 0x4007e4

Nov 10 03:43:30 localhost kernel: SIG 2: SIG_DFL

......

Nov 10 03:43:30 localhost kernel: Thread count in proc(pid=5703):3

7.    PThread线程库是如何创建线程的

7.1  Glibc源程序RPM包的下载

 Pthread library的实现在glibc中。

wget http://vault.centos.org/6.5/os/Source/SPackages/glibc-2.12-1.132.el6.src.rpm

7.2  RPM包中获取Glibc的源码

1)      需要先安装rpm-build

yum install rpm-build

2)      mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}

3)      编译但编译完成后不删除源程序

rpmbuild --recompile glibc-2.12-1.132.el6.src.rpm

4)      编译完成后,Pthread的源程序在~/rpmbuild/BUILD/glibc-2.12-2-gc4ccff1/nptl目录下

7.3  pthread_create()的实现

Linux内核提供了一个系统调用clone()可以创建线程。该系统调用的实现函数是sys_clone()

在文件nptl/sysdeps/pthread/createthread.c中,我们可以看到传递给clone()系统调用的flags参数如下:

 int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL

                                    | CLONE_SETTLS | CLONE_PARENT_SETTID

                                    | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM

#if __ASSUME_NO_CLONE_DETACHED == 0

                                    | CLONE_DETACHED

#endif

                                    | 0);

我们可以看出,因为pthread_create()传递给sys_clone()flags参数中包含了CLONE_VM |  CLONE_FILES | CLONE_SIGNAL标志,所以其创建出来的线程都共享一个地址空间、一套文件描述符表和一套信号处理程序表。

8.    结论

通过前面对Linux内核源码的分析和实验,我们可以得出来一个结论:Linux内核的线程实现是货真价实的。因此,Linux上的多线程实现是真正的多线程实现。

9.    参考资料

1.       Daniel P. Bovet & Marco Cesati.  Uderstanding the Linux Kernel. O’Reilly Media, Inc. 2006.

2.       Michael KerrisK. The Linux Programming Interface. No Starch Press, Inc. 2010.

Syscalls on x86/x64: http://stackoverflow.com/questions/9506353/how-to-invoke-a-system-call-via-sysenter-in-inline-assembly-x86-amd64-linux
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核中,可以通过内核线程(kernel thread)来实现多线程。内核线程是独立运行在内核空间的标准进程,与普通进程相比,内核线程没有独立的地址空间,mm指针被设置为NULL,只在内核空间运行,不切换到用户空间去。内核线程可以被调度和抢占。 在Linux内核中,可以使用kthread_run()函数来创建内核线程。这个函数接受一个执行函数和一个参数作为参数,可以在执行函数中完成一些后台任务。创建的内核线程可以通过kthread_stop()函数来停止。 在早期的Linux 2.6版本中,可以使用kernel_thread()函数来创建内核线程。但在较新的版本中已不推荐使用该方式,因为在4.1版本中不再使用export。使用kernel_thread()创建的非内核线程需要在其执行函数中调用daemonize()函数来释放资源。 除了以上两种方式,还可以使用kthread_create()函数来创建内核线程。这个函数与kthread_run()类似,用法也相似。 总之,在Linux内核中可以通过内核线程来实现多线程的功能,这些内核线程可以在后台执行一些任务,具有调度和抢占的特性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Linux内核多线程](https://blog.csdn.net/Frank_sample/article/details/116455771)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值