首页 > 系统相关 >【进程与线程】Linux中进程与线程的区别

【进程与线程】Linux中进程与线程的区别

时间:2022-12-03 16:44:39浏览次数:49  
标签:task struct CLONE 线程 Linux 进程 clone

1.线程的创建方法

创建线程具体调用pthread_create函数,这个函数实在glibc库中实现。在glibc中pthread_create的调用路径是__pthread_create_2_1->create_thread。其中create_thread很重要,它设置了创建线程时使用的各种flag标记。

// 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);
 ...
}

这里我们传入了 CLONE_VM、CLONE_FS、CLONE_FILES 等标记,接下来的 do_clone 最终会调用一段汇编程序,在汇编里进入 clone 系统调用,之后会进入内核中进行处理。

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

二、内核中对线程的表示

进程和线程的相同点要远远大于不同点。主要依据就是在 Linux 中,无论进程还是线程,都是抽象成了 task 任务,在源码里都是用 task_struct 结构来实现的。

image

我们来看 task_struct 具体的定义,它位于 include/linux/sched.h

//file:include/linux/sched.h

struct task_struct{
    // 1.1 task状态
    volatile long state;

    // 1.2 进程线程的pid
    pid_t pid;
    pid_t tgid;   // 线程所属进程的PID

    // 1.3 task树关系:父进程、子进程、兄弟进程
    struct task_struct __rcu *parent;
    struct list_head children;
    struct list_head sibling;
    struct task_struct *group_leader;

    // 1.4 task调度优先级
    int prio,static_prio,normal_prio;
    unsigned int rt_priority;

    // 1.5 地址空间
    struct mm_struct *mm,*active_mm;

    // 1.6 文件系统信息(当前目录等)
    struct fs_struct *fs;

    // 1.7 打开的文件信息
    struct files_struct *files;

    // 1.8 namespaces
    struct nsproxy *nsproxy;

}

对于线程来讲,所有的字段都是和进程一样的(本来就是一个结构体来表示的)。包括状态、pid、task 树关系、地址空间、文件系统信息、打开的文件信息等等字段,线程也都有。

进程和线程的相同点要远远大于不同点,本质上是同一个东西,都是一个 task_struct !正因为进程线程如此之相像,所以在 Linux 下的线程还有另外一个名字,叫轻量级进程。

pid 和 tgid 这两个字段。在 Linux 中,每一个 task_struct 都需要被唯一的标识,它的 pid 就是唯一标识号。

//file:include/linux/sched.h
struct task_struct {
 ......
 pid_t pid;
 pid_t tgid;
}

对于进程来说,这个 pid 就是我们平时常说的进程 pid。

对于线程来说,我们假如一个进程下创建了多个线程出来。那么每个线程的 pid 都是不同的。但是我们一般又需要记录线程是属于哪个进程的。这时候,tgid 就派上用场了,通过 tgid 字段来表示自己所归属的进程 ID。

image

这样内核通过 tgid 可以知道线程属于哪个进程。

三、线程创建过程

3.1 进程创建

进程线程创建的时候,使用的函数看起来不一样。但实际在底层实现上,都是通过同一个函数实现的

image

fork 调用主要就是执行了 do_fork 函数,fork 函数调用 do_fork 的传的参数分别是SIGCHLD、0,0,NULL,NULL。

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

do_fork 函数又调用 copy_process 完成进程的创建。

//file:kernel/fork.c
long do_fork(...)
{
    //复制一个 task_struct 出来
    struct task_struct *p;
    p = copy_process(clone_flags, ...);
    ...
}

3.2线程的创建

库函数 pthread_create 会调用到 clone 系统调用,为其传入了一组 flag。

//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, ...);
    ...
}

clone 系统调用的实现

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

同样,do_fork 函数还是会执行到 copy_process 来完成实际的创建。

3.3 进程线程创建区别

可见和创建进程时使用的 fork 系统调用相比,创建线程的 clone 系统调用几乎和 fork 差不多,也一样使用的是内核里的 do_fork 函数,最后走到 copy_process 来完整创建。

不过创建过程的区别是二者在调用 do_fork 时传入的 clone_flags 里的标记不一样!。

  • 创建进程时的 flag:仅有一个 SIGCHLD
  • 创建线程时的 flag:包括 CLONE_VM、CLONE_FS、CLONE_FILES、CLONE_SIGNAL、CLONE_SETTLS、CLONE_PARENT_SETTID、CLONE_CHILD_CLEARTID、CLONE_SYSVSEM。

关于这些 flag 的含义,我们选几个关键的做一个简单的介绍,后面介绍 do_fork 细节的时候会再次涉及到。

  • CLONE_VM: 新 task 和父进程共享地址空间
  • CLONE_FS:新 task 和父进程共享文件系统信息
  • CLONE_FILES:新 task 和父进程共享文件描述符表

四、do_fork()系统调用

进程和线程创建都是调用内核中的 do_fork 函数来执行的。在 do_fork 的实现中,核心是一个 copy_process 函数,它以拷贝父进程(线程)的方式来生成一个新的 task_struct 出来。

//file:kernel/fork.c
long do_fork(unsigned long clone_flags, ...)
{
 //复制一个 task_struct 出来
 struct task_struct *p;
 p = copy_process(clone_flags, stack_start, stack_size,
    child_tidptr, NULL, trace);

 //子任务加入到就绪队列中去,等待调度器调度
 wake_up_new_task(p);
 ...
}

在创建完毕后,调用 wake_up_new_task 将新创建的任务添加到就绪队列中,等待调度器调度执行。

//file:kernel/fork.c
static struct task_struct *copy_process(...)
{
 //4.1 复制进程 task_struct 结构体
 struct task_struct *p;
 p = dup_task_struct(current);
 ...

 //4.2 拷贝 files_struct
 retval = copy_files(clone_flags, p);

 //4.3 拷贝 fs_struct
 retval = copy_fs(clone_flags, p);

 //4.4 拷贝 mm_struct
 retval = copy_mm(clone_flags, p);

 //4.5 拷贝进程的命名空间 nsproxy
 retval = copy_namespaces(clone_flags, p);

 //4.6 申请 pid && 设置进程号
 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;

 ......
}

copy_process 先是复制了一个新的 task_struct 出来,然后调用 copy_xxx 系列的函数对 task_struct 中的各种核心对象进行拷贝处理,还申请了 pid 。

4.1 复制task_struct结构体

上面调用 dup_task_struct 时传入的参数是 current,它表示的是当前任务。在 dup_task_struct 里,会申请一个新的 task_struct 内核对象,然后将当前任务复制给它。需要注意的是,这次拷贝只会拷贝 task_struct 结构体本身,它内部包含的 mm_struct 等成员不会被复制。

image

具体代码:

//file:kernel/fork.c
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
 //申请 task_struct 内核对象
 tsk = alloc_task_struct_node(node);
 //复制 task_struct
 err = arch_dup_task_struct(tsk, orig);
 ...
}

其中 alloc_task_struct_node 用于在 slab 内核内存管理区中申请一块内存出来。

//file:kernel/fork.c
static struct kmem_cache *task_struct_cachep;
static inline struct task_struct *alloc_task_struct_node(int node)
{
`  return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);`
}

申请完内存后,调用 arch_dup_task_struct 进行内存拷贝。

//file:kernel/fork.c
int arch_dup_task_struct(struct task_struct *dst,
         struct task_struct *src)
{
    *dst = *src;
    return 0;
}

4.2 拷贝打开文件列表

创建线程调用 clone 系统调用的时候,传入了一堆的 flag,其中有一个就是 CLONE_FILES。如果传入了 CLONE_FILES 标记,就会复用当前进程的打开文件列表 - files 成员。

image

对于创建进程来讲,没有传入这个标志,就会新创建一个 files 成员出来。

image

继续看 copy_files 具体实现。

//file:kernel/fork.c
static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
{
 struct files_struct *oldf, *newf;
 oldf = current->files;

 if (clone_flags & CLONE_FILES) {
  atomic_inc(&oldf->count);
  goto out;
 }
 newf = dup_fd(oldf, &error);
 tsk->files = newf;
 ...
}

从代码看出,如果指定了 CLONE_FILES(创建线程的时候),只是在原有的 files_struct 里面 +1 就算是完事了,指针不变,仍然是复用创建它的进程的 files_struct 对象。

这就是进程和线程的其中一个区别,对于进程来讲,每一个进程都需要独立的 files_struct。但是对于线程来讲,它是和创建它的线程复用 files_struct 的。

4.3 拷贝文件目录信息

创建线程的时候,传入的 flag 里也包括 CLONE_FS。如果指定了这个标志,就会复用当前进程的文件目录 - fs 成员。

image

对于创建进程来讲,没有传入这个标志,就会新创建一个 fs 出来。

image

copy_fs 的实现。

//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;
}

和 copy_files 函数类似,在 copy_fs 中如果指定了 CLONE_FS(创建线程的时候),并没有真正申请独立的 fs_struct 出来,近几年只是在原有的 fs 里的 users +1 就算是完事。

而在创建进程的时候,由于没有传递这个标志,会进入到 copy_fs_struct 函数中申请新的 fs_struct 并进行赋值拷贝。

4.4 拷贝内存地址空间

创建线程的时候带了 CLONE_VM 标志,而创建进程的时候没带。接下来在 copy_mm 函数 中会根据是否有这个标志来决定是该和当前线程共享一份地址空间 mm_struct,还是创建一份新的。

//file:kernel/fork.c
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
	struct mm_struct *mm, *oldmm;
	oldmm = current->mm;

	if (clone_flags & CLONE_VM) {
	   atomic_inc(&oldmm->mm_users);
	   mm = oldmm;
	   goto good_mm;
	}
	mm = dup_mm(tsk);
	good_mm:
	return 0; 
}

对于线程来讲,由于传入了 CLONE_VM 标记,所以不会申请新的 mm_struct 出来,而是共享其父进程的。

image

多线程程序中的所有线程都会共享其父进程的地址空间。

image

image

而对于多进程程序来说,每一个进程都有独立的 mm_struct(地址空间)。

image

因为在内核中线程和进程都是用 task_struct 来表示,只不过线程和进程的区别是会和创建它的父进程共享打开文件列表、目录信息、虚拟地址空间等数据结构,会更轻量一些。所以在 Linux 下的线程也叫轻量级进程。

在打开文件列表、目录信息、内存虚拟地址空间中,内存虚拟地址空间是最重要的。因此区分一个 Task 任务该叫线程还是该叫进程,一般习惯上就看它是否有独立的地址空间。如果有,就叫做进程,没有,就叫做线程。

这里展开多说一句,对于内核任务来说,无论有多少个任务,其使用地址空间都是同一个。所以一般都叫内核线程,而不是内核进程。

五、结论

创建线程的整个过程介绍完了。总结一下,对于线程来讲,其地址空间 mm_struct、目录信息 fs_struct、打开文件列表 files_struct 都是和创建它的任务共享的。

image

但是对于进程来讲,地址空间 mm_struct、挂载点 fs_struct、打开文件列表 files_struct 都要是独立拥有的,都需要去申请内存并初始化它们。

image

在 Linux 内核中并没有对线程做特殊处理,还是由 task_struct 来管理。从内核的角度看,线程本质上还是一个进程。只不过和普通进程比,稍微“轻量”了那么一些。

线程具体能轻量多少呢?通过进程和线程的上下文切换开销测试。进程的测试结果是一次上下文切换平均 2.7 - 5.48 us 之间。线程上下文切换是 3.8 us左右。总的来说,进程线程切换还是没差太多。











转载文章:
https://zhuanlan.zhihu.com/p/575360641

标签:task,struct,CLONE,线程,Linux,进程,clone
From: https://www.cnblogs.com/Wangzx000/p/16945184.html

相关文章

  • 在子线程中更新UI组件
    androidUI界面更新UI线程属于主线程,当涉及到需要更新UI组件的问题时,需要在主线程中更新,另起线程会出现线程崩溃,当子线程需要修改主线程的UI组件时,需要通过发送消息来进......
  • 常见Linux命令
    前言Lessismore如有错误还请指正Linux常见命令ifconfigip地址file文件type显示命令的类型ls查看当前目录下文件ls-la查看隐藏文件pwd路径cp拷贝mv......
  • Linux常见基本维护查看命令(1)
    1、如何看当前Linux系统有几颗物理CPU和每颗CPU的核数?[kiosk@rhce8-exam43~]$cat/proc/cpuinfo|grep-c'physicalid'4[kiosk@rhce8-exam43~]$cat/proc/cpuinfo|gr......
  • 你在终端启动的进程,最后都是什么下场?(下)
    你在终端启动的进程,最后都是什么下场?(下)在上期文章你在终端启动的进程,最后都是什么下场?(上)当中我们介绍了前台进程最终结束的几种情况,在本篇文章当中主要给大家介绍后台进程......
  • 线程的活跃性
    一、死锁有A,B两把锁,t1持有A想获取B,t2持有B想获取A,导致t1t2两个线程最终都进入阻塞状态的现象publicclassTest8{privatefinalstaticLoggerLOGGER=LoggerF......
  • linux运维之道学习笔记
    linux常用命令1、find命令   find/"*.log"查找/目录下.log结尾的档案   find/-mtime-3查找/目录下三天内被修改的档案   find/-mtime+4 查......
  • 使用Kernel 2.6版本的Linux系统运行dbca创建数据库实例时报错ORA-27125
    问题描述:使用Kernel2.6版本的Linux系统运行dbca创建数据库实例时报错ORA-27125,如下所示:系统:rhel6.564位数据库:oracle10.2.0.164位异常原因:该异常与linuxhugetlb有关.......
  • JUC高级篇-第2章 多线程锁
    1.乐观锁与悲观锁悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。适合写操作多的场景,先加锁可以保......
  • Linux 系统环境监测
    Linux系统环境监测Linux系统环境主要监测CPU、内存、磁盘I/O和网络流量。1.CPU(1)查看CPU的负载情况:uptime可以通过uptime查看系统整体的负载情况。如果服务器的CPU为......
  • 控制多个线程的执行顺序
    记录了如何控制多个线程的执行顺序,以练习题的形式记录一、两个线程顺序执行题目描述:线程t1会打印A,线程t2会打印B,实现先打印B再打印A题目分析:实现的关键是线程1执行......