上节详细学习了进程的创建,通过实例学习了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
从上面的结果可以看出
我们通过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是不一个东西。
我们用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系统调用中定义的,可以通过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来创建一个子进程的话,父进程的资源是通过copy的方式给子进程。其中就涉及到COW技术,当父子双方一方去写资源时才发生分裂。
举个生活中的例子,当母亲剩下儿子后,儿子还小就和父母亲在同一个屋子生活,当儿子慢慢的长大,就需要单独一个屋子,这时候会给儿子单独盖一个屋子来着,这就相当于分裂。
vfork创建子进程时,最终会调用clone的系统调用,传递的参数是CLONE_VM, CLONE_VFORK. 这代表的意思是父子进程共享内存资源。
vfork和fork的最大区别就是共享mm的资源,只要其中一方修改mm的资源,另外一方就会看到。
前面说了,一个进程中如果有多个线程,那这些线程都会共享进程的资源的。线程也称为轻量级进程(LWP),之所有轻量就是所有的资源和父进程共享,调度的话上下文切换的时间就比较少了。
既然线程共享父进程的所有资源,所以linux内核通过pthread_create来创建线程的时候,最终传递给clone的参数都是CLONE_VM,CLONE_FILES,CLONE_FS等,这说明所有的资源共享,这样就可以实现线程了。
linux就是通过这样的方式来实现用户线程的。这样一来父子进程共享了所有的资源,共享了所有的资源,则这就是线程。
但是linux内核没有线程的概念,内核中只认task_struct结构,只要是task_struct结构就可以参与调度。所以内核中并没有区分进程和线程。