首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >线程的创建以及线程的本质

线程的创建以及线程的本质

作者头像
DragonKingZhu
发布2020-03-24 17:24:55
1.5K0
发布2020-03-24 17:24:55
举报

上节详细学习了进程的创建,通过实例学习了fork和vfork的区别。本节将学习线程的创建,只涉及应用层的线程,内核线程的创建在后面学习。

应用线程的创建

应用线程的创建,想必大家都有所了解。使用pthread_create库函数来创建应用线程。通过一个简单的例子来看下。

我们先来看下pthread_create的参数,通过man pthread_create

NAME
       pthread_create - create a new thread
 
SYNOPSIS
       #include <pthread.h>
 
       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
 
       Compile and link with -pthread.

pthread_create是创建一个新线程,参数thread是threadID, 可以通过pthread_self返回,此threadID是遵循POSIX的标准,和linux内核中定义的threadID是不一样的,待会通过实例说明

attr是创建此thread的属性,可以通过pthread_attr_init函数来初始化attr参数。

start_routine就是线程的回调,当创建线程成功时,就会调用此函数指针,而arg就是此函数指针的参数。

#include <pthread.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/syscall.h>
 
static void* thread_call(void* arg)
{
 printf("create thread success! arg=%d\n",arg);
 return NULL;
}
 
int main()
{
 int ret;
 pthread_t thread;
 
 ret = pthread_create(&thread, NULL, thread_call, 100 );
 if(ret == -1)
     printf("create thread faild!\n");
 
 ret = pthread_join(thread, NULL);
 if(ret == -1)
     printf("pthread join failed!\n");
 
 return ret;
}

运行结果,编译的时候需要指定-pthread

root@ubuntu:zhuxl$ ./a.out
create thread success!, arg=100

接下来再看一个实例,来证实下pid, tid, 和pthread_self的三者区别

#include <pthread.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/syscall.h>
 
static pid_t gettid(void)
{
   return syscall(SYS_gettid);
}
 
static void* thread_call(void* arg)
{
    printf("thread_create success!\n");
    printf("thread_call pid=%d, tid=%d, threadID=%ld\n",getpid(), gettid(), pthread_self());
    while(1);
    return NULL;
}
 
int main()
{
    int ret;
    pthread_t thread;
 
    printf("pid=%d, tid=%d, threadID=%ld\n",getpid(), gettid(), pthread_self());
 
    ret = pthread_create(&thread, NULL, thread_call, NULL );
    if(ret == -1)
        printf("create thread faild!\n");
 
    ret = pthread_join(thread, NULL);
    if(ret == -1)
        printf("pthread join failed!\n");
 
    return ret;
}

运行结果:

root@ubuntu:zhuxl$ ./a.out
pid=101104, tid=101104, threadID=140298706515776
thread_create success!
thread_call pid=101104, tid=101105, threadID=14029869802060

从上面的结果可以看出

  • 进程中所有线程的pid都是相同的
  • 要想获得一个进程中所有线性的id,需要通过tid号,可以通过gettid函数
  • pthread_self返回的ID和gettid返回的ID是不一样的。
  • pthread_self返回的ID是遵循POSIX的标准,而gettid()返回的ID是linux内核自定义的

我们通过PS来看下进程和线程的关系

root@ubuntu:$ ps -eLf
UID         PID   PPID    LWP  C NLWP STIME TTY          TIME CMD
root      101104  90968 101104  0    2 20:27 pts/0    00:00:00 ./a.out
root      101104  90968 101105 99    2 20:27 pts/0    00:03:40 ./a.out

可以看到通过ps来查看PID是相同的,都是通过getpid获取的。LWP(light weight process)轻量级进程,也就是线程了。可以看到线程的ID是不一样的。

可以通过man gettid来进一步证实

NAME
       gettid - get thread identification
 
SYNOPSIS
       #include <sys/types.h>
 
       pid_t gettid(void);
 
       Note: There is no glibc wrapper for this system call; see NOTES.
 
DESCRIPTION
       gettid()  returns  the  caller's  thread  ID  (TID).  In a single-threaded process, the thread ID is equal to the process ID (PID, as
       returned by getpid(2)).  In a multithreaded process, all threads have the same PID, but each one  has  a  unique  TID.   For  further
       details, see the discussion of CLONE_THREAD in clone(2).

在单个线程中,threadID和processID是相同的,都是通过getpid函数获取的。在多线程的进程中,所有的线程都有相同的PID,但是各个线程页拥有一个独一无二的TID.

再来通过man pthread_self看下

Thread  IDs  are guaranteed to be unique only within a process.  A thread ID may be reused after a terminated thread has been joined,
or a detached thread has terminated.
 
The thread ID returned by pthread_self() is not the same thing as the kernel thread ID returned by a call to gettid(2).

通过pthread_self返回的ID和gettid返回的kernel thread ID是不一个东西。

pthread_create真正调用

我们用strace来跟踪下pthread_create最后调用的系统调用是啥

root@ubuntu:zhuxl$ strace ./a.out
execve("./a.out", ["./a.out"], 0x7fffdb498be0 /* 56 vars */) = 0
brk(NULL)                               = 0x56046931d000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=99218, ...}) = 0
mmap(NULL, 99218, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f31ab97f000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000b\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=144976, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f31ab97d000
mmap(NULL, 2221184, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f31ab552000
mprotect(0x7f31ab56c000, 2093056, PROT_NONE) = 0
mmap(0x7f31ab76b000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19000) = 0x7f31ab76b000
mmap(0x7f31ab76d000, 13440, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f31ab76d000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f31ab161000
mprotect(0x7f31ab348000, 2097152, PROT_NONE) = 0
mmap(0x7f31ab548000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f31ab548000
mmap(0x7f31ab54e000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f31ab54e000
close(3)                                = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f31ab97a000
arch_prctl(ARCH_SET_FS, 0x7f31ab97a740) = 0
mprotect(0x7f31ab548000, 16384, PROT_READ) = 0
mprotect(0x7f31ab76b000, 4096, PROT_READ) = 0
mprotect(0x5604674c3000, 4096, PROT_READ) = 0
mprotect(0x7f31ab998000, 4096, PROT_READ) = 0
munmap(0x7f31ab97f000, 99218)           = 0
set_tid_address(0x7f31ab97aa10)         = 102191
set_robust_list(0x7f31ab97aa20, 24)     = 0
rt_sigaction(SIGRTMIN, {sa_handler=0x7f31ab557cb0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f31ab564890}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {sa_handler=0x7f31ab557d50, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7f31ab564890}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
gettid()                                = 102191
getpid()                                = 102191
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
brk(NULL)                               = 0x56046931d000
brk(0x56046933e000)                     = 0x56046933e000
write(1, "pid=102191, ttid=102191, threadI"..., 50pid=102191, ttid=102191, threadID=139851308967744
) = 50
mmap(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f31aa960000
mprotect(0x7f31aa961000, 8388608, PROT_READ|PROT_WRITE) = 0
clone(child_stack=0x7f31ab15ffb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f31ab1609d0, tls=0x7f31ab160700, child_tidptr=0x7f31ab1609d0) = 102192
thread_create success!
thread_call pid=102191, ttid=102192, threadID=139851300472576

可以看到最终会调用到clone系统调用,我们之前学过的fork & vfork最终页都是调用的clone系统调用。

而这三者的区别就是flag参数不一样。

名称

flag参数

fork

CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD

vfork

CLONE_VM | CLONE_VFORK | SIGCHLD

pthread_create

CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID

CLONE_XX各个字段含义

CLONE_XX各个字段的含义,是在clone系统调用中定义的,可以通过man clone来查看

字段

man文的英文解释

中文解释

CLONE_VM

If CLONE_VM is set, the calling process and the child process run in the same memory space. If CLONE_VM is not set, the child process runs in a separate copy of the memory space of the calling process

意思就是如何设置CLONE_VM了,代表子进程和父进程共享mm资源 如果CLONE_VM没有设置,父子进程运作在各子的内存空间

CLONE_VFORK

If CLONE_VFORK is set, the execution of the calling process is suspended until the child releases its virtual memory resourcesvia a call to execve(2) or _exit(2) (as with vfork(2)).

如果设置了CLONE_VFORK, 父进程挂起,直到子进行运行退出完毕,父进程CIA可以运行

CLONE_FS

If CLONE_FS is set, the caller and the child process share the same filesystem information. If CLONE_FS is not set, the child process works on a copy of the filesystem information of the calling process

如果CLONE_FS设置,父子进程共享相同的文件系统资源 如果CLONE_FS没有设置,子进程会对父进行的文件系统资源做一份拷贝

CLONE_FILES

If CLONE_FILES is set, the calling process and the child process share the same file descriptor table If CLONE_FILES is not set, the child process inherits a copy of all file descriptors opened in the calling process

如果CLONE_FILES设置,父子进程共享相同的文件资源 如果CLONE_FILES没设置,子进程会copy父进程的文件资源信息一份

这里只简单的列举了几个,还有很多没列举,有兴趣的同学可以通过man clone去查看各个字段的含义

这里可以得出一个结论:CLONE_VM, FILES, FS,SIGHAND等等,如果这些资源设置就代表父子进程共享相同的资源。如果没设置则是子进程会对父进程的资源做一份copy动作。

fork,vfork,pthread_create的更深层次的理解

之前我们在进程的基本概念中说过,进程是资源分配的基本单位,线程是系统调度的最新单位。既然进程是资源分配的单位,那一个进程中就拥有很多资源。

如果去创建一个子进程的话,就需要将父进程的资源看按照怎么样的方式给子进程。

fork创建一个子进程

如果使用fork来创建一个子进程的话,父进程的资源是通过copy的方式给子进程。其中就涉及到COW技术,当父子双方一方去写资源时才发生分裂。

举个生活中的例子,当母亲剩下儿子后,儿子还小就和父母亲在同一个屋子生活,当儿子慢慢的长大,就需要单独一个屋子,这时候会给儿子单独盖一个屋子来着,这就相当于分裂。

vfork创建一个子进程

vfork创建子进程时,最终会调用clone的系统调用,传递的参数是CLONE_VM, CLONE_VFORK. 这代表的意思是父子进程共享内存资源。

vfork和fork的最大区别就是共享mm的资源,只要其中一方修改mm的资源,另外一方就会看到。

pthread_create来创建一个线程

前面说了,一个进程中如果有多个线程,那这些线程都会共享进程的资源的。线程也称为轻量级进程(LWP),之所有轻量就是所有的资源和父进程共享,调度的话上下文切换的时间就比较少了。

既然线程共享父进程的所有资源,所以linux内核通过pthread_create来创建线程的时候,最终传递给clone的参数都是CLONE_VM,CLONE_FILES,CLONE_FS等,这说明所有的资源共享,这样就可以实现线程了。

linux就是通过这样的方式来实现用户线程的。这样一来父子进程共享了所有的资源,共享了所有的资源,则这就是线程。

但是linux内核没有线程的概念,内核中只认task_struct结构,只要是task_struct结构就可以参与调度。所以内核中并没有区分进程和线程。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 应用线程的创建
  • pthread_create真正调用
  • CLONE_XX各个字段含义
  • fork,vfork,pthread_create的更深层次的理解
    • fork创建一个子进程
      • vfork创建一个子进程
        • pthread_create来创建一个线程
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档