提到进程和线程的区别,有句很经典的话:进程是资源分配的基本单位,线程是调度的基本单位。那么它俩本质上是怎么区分的呢?
下面就让我们来看一下Linux下进程(process)和线程(thread)的本质区别。
首先看下《LKD》里对进程的定义,
A process is a program (object code stored on some media) in the midst of execution.
也就是处于运行状态的程序。除了程序代码,进程还包含很多其它资源,依然来自《LKD》里的定义,
They also include a set of resources such as open files and pending signals,
internal kernel data, processor state, a memory address space with one or more memory
mappings, one or more threads of execution, and a data section containing global variables.
ps:我们写程序时有时会加上打印输出,其实就是用的进程的资源—open files去输出的,也就是标准输出流句柄
从这可以看出进程就是正在执行的程序加上相关资源的一个集合体,
内核为每个进程生成一个进程描述符(process descriptor),其类型是struct task_struct。进程描述符是个很大的结构体,包含了进程所有相关的信息。
内核会把进程描述符放到一个叫task_list的双向循环链表里。(图片来自《LKD》)
这个task_list就是给CPU来调度的,可以说CPU调度的就是这些进程描述符。
先看下《LKD》里线程的定义,
Threads of execution, often shortened to threads, are the
objects of activity within the process
. Each thread includes a unique program counter, process stack, and set of processor registers.
线程是进程里的活动对象。为什么这么说?因为我们都知道同一进程里的线程之间是可以直接共享资源的,不需要像进程间共享资源搞出很多方法,所以从外部来看线程是处于进程里运行的。
Linux内核为线程也生成进程描述符,这个进程描述符也会放到task_list里。这是linux线程的特别之处,并不区分线程和进程,因为两者都有进程描述符。
到这里,可能有点晕了,可以看下一节的图释。
假设我们有个程序代码如下,
#include
int main(void)
{
printf("hello world\n");
return 0;
}
编译完后运行,那么这个程序对应的进程示意图如下(蓝色部分表示与资源相关的成员,大部分是指针),
这段程序没有创建线程,只有一个进程描述符,进程描述符里与资源相关的成员会指向该进程所拥有的资源。
下面我们看一个创建线程的程序代码,
#include
#include
#include
int share = 100;
void * thread_fn(void *arg)
{
printf("hello %s: %d\n", __func__, share);
}
int main()
{
pthread_t tid;
int err;
err = pthread_create(&tid, NULL, thread_fn, NULL); // 创建线程
if (err != 0)
{
printf("Error: in pthread_create()\n");
return 1;
}
printf("hello %s: %d\n", __func__, share);
sleep(1);
return 0;
}
输出如下,
运行时这个程序对应的进程示意图如下,
程序开始运行时linux会生成进程描述符1,运行过程中创建线程去执行thread_fn(),此时linux会生成进程描述符2。这两个进程描述符里与资源有关的成员都指向相同的资源,这就是线程间共享资源的原理。
对于没有创建线程的例子,也可以认为该进程里只包含一个线程,称为单线程进程,进程里的资源被这个单线程独享。
从全局来看,操作系统上一般会有多个程序在运行,反映到内核里,如下图所示
在task_list里存放进程描述符,有的属于单线程进程的,有的属于多线程进程里单个线程的。
对于内核来说,它分配资源时是按照进程来分配的,分配完资源后如果进程里创建了多个线程,那么多个线程间就共享相同的资源,向外表现为一个进程里多个线程在运行。而调度就是调度task_list里的一个个进程描述符,所以CPU调度的基本单位是线程,不是进程(单线程进程也可以视为独立线程)。
ps:从内核的视角来看,没有外面的红色虚线框,内核只负责调度单个的进程描述符,而不关心这个进程描述符属于谁的。
每个进程都有自己独一无二的进程id号,存在进程描述符(也就是struct task_struct)里的pid成员里,
下面的代码展示线程的pid和tgid,
#include
#include
#include
#include
void * thread_fn(void *arg)
{
printf("thread_fn() pid: %d, thread pid: %lu\n", getpid(), syscall(SYS_gettid));
}
int main()
{
pthread_t tid;
int err;
err = pthread_create(&tid, NULL, thread_fn, NULL); // 创建线程
if (err != 0)
{
printf("Error: in pthread_create()\n");
return 1;
}
printf("main() pid: %d\n", getpid());
sleep(1);
return 0;
}
输出如下,
可以看出主线程里调用getpid()得到的是主线程的进程描述符里的pid值,而在执行thread_fn的线程里调用getpid()返回的则是该线程的进程描述符里的tgid值,这个值和主线程的pid是相等的。
想要查看线程自己本身的pid,需要使用syscall(SYS_gettid)函数调用,当然还有一些其它方法,可以看出线程自身也有pid,且与主线程的pid值不一样,这也证明了内核给每个线程都会分配一个进程描述符。
只要抓住进程描述符
和相关资源
这2个概念,就很容易弄懂linux下进程和线程的区别了。(似乎叫线程描述符更合适点)
如果有写的不对的地方,希望能留言指正,谢谢阅读。