Linux中进程和线程之间的区别

时间:2020-03-21 11:43:23  来源:igfitidea点击:

我们经常听到人们经常使用两个词。
一个是“进程”,另一个是“线程”。
哪个是进程,哪个是线程,以及区分这两者的方法通常会使许多人感到困惑。

在本文中,我们将尝试在Linux操作系统的上下文中发现它们中的每一个,并了解它们的主要区别。

让我们首先从进程开始,然后再转到线程。
我们可以找到的关于进程的最常见定义是“它是程序的一个实例,正在执行中”。
那有什么意思?

在某种程度上,我们实际上可以将进程与OOP中的对象进行比较(即,面向对象的编程)。
在面向对象的编程中,对象也被定义为类的实例。
每个对象都有自己的值和特征,但是通过将类视为蓝图可以创建对象。
我们可以从同一类创建许多对象。

相关:面向对象编程的基础

同样,如果我们在Linux中有一个文本编辑器程序,例如“ vi”,则userA可以打开一个文本编辑器(“ vi”程序的一个实例),userB也可以打开相同的文本编辑器(“ vi”的另一个实例)程序)。
让我们尝试在Linux中这样做。

以下命令在两个单独的终端上触发。
这将打开vi文本编辑器,并让我们创建一个新的文本文件。

user1@localhost:~$vi testfile1

user2@localhost:~$vi testfile2

现在让我们使用另一个终端来查找系统上正在运行多少个vi进程。

root@localhost:~# ps aux|grep vi
user2     6167  0.0  0.8  52616  8464 pts/1    S+   11:55   0:00 vi testfile2
user1     6168  0.0  0.8  52616  8448 pts/0    S+   11:55   0:00 vi testfile1

从上面的输出中我们可以清楚地看到,我们的“ vi”程序的每个实例都创建了自己的进程。
这就是流行的定义“进程是正在执行的程序的实例”的原因。
如果将它与前面提到的OOP进行比较,则程序类似于类,进程类似于对象(某种程度上,不是100%)。

进程本质上是完全动态的。
随着CPU执行指令,它在不断变化。
Linux内核的设计方式使每个进程都有自己的权限。
一个进程中的问题不会影响系统中运行的其他进程。
这是因为每个进程都有其单独的地址空间。

系统中的每个进程都由一个称为PID的数字标识(在我们的“ vi”进程的先前显示的输出的第二列中提到的数字是PID编号。
如果我们注意到,这两个进程都有自己的PID编号)。
该编号由内核分配,并在进程退出时释放以供重用。
即使我们运行的命令在一秒钟内完成,它仍然会创建一个带有PID编号的进程。
如果我们对有关Linux中进程的更多管理详细信息感兴趣,我建议阅读以下文章。

阅读:管理Linux进程

如果要在Linux上做一些有价值的事情,则需要一种称为“系统调用”的系统。
没有系统调用,我们将无法完成大部分工作(读取文件,写入文件,打开端口)。

系统调用不过是与操作系统进行交互的一种方式,因此操作系统可以执行我们没有直接权限的操作。
即使创建一个进程,我们也需要请求操作系统为我们执行此操作(当我说操作系统时,我在这里指的是内核)。
记得我们曾经提到过,每个进程(即:程序的实例:),现在我们已经知道了定义)都有一个与之关联的唯一编号,称为PID?
该数字也由内核分配。
因此,即使是系统调用。
许多程序将读写不同的文件,所有这些都是通过系统调用来实现的。

为了使程序员的生活更轻松,几乎所有内容都有C库函数(由GNU C库提供)。
如前所述,几乎在系统上执行的每个操作(读取,写入,创建进程等)都需要使用系统调用。
作为程序员,我们不必担心要为哪个操作使用哪个系统调用,以及如何触发特定的系统调用等。
我们可以使用带有适当函数参数的C库函数,这些函数将内部触发系统调用为你。
例如,我不需要知道将文件写入文件的系统调用(我将使用名为“ write”的库函数,是的,在大多数情况下,系统调用的名称与库函数的外观相似)。

Linux中的每个进程都是由父进程使用一个称为fork的库函数/例程创建的(存在一个与函数“ fork”同名的系统调用)。
我实际上必须说的是INIT/systemd以外的所有其他内容(Linux中使用PID 1的第一个进程)是使用fork方法创建的。

有一个名为fork()的系统调用。
可用的相应库函数也称为fork。
传统上,fork()是创建新进程的系统调用。
但是这些天来,fork()系统调用实际上已经被其他东西代替了(尽管系统调用本身已经存在,但很少使用。
它仍然可用于向后兼容。

在最近的所有系统中,用于创建进程的系统调用都称为clone(),而不是fork()。
clone()与fork()非常相似,但是具有更多功能,并且本质上是通用的。
正如clone()可以完成fork()所做的事情一样,再加上其他一些事情,甚至是名为fork()的标准c库函数,都可以调用clone()。

最重要的是,... clone()在所有现代系统中替代了fork()。
即使我们使用c库提供的fork函数,该函数也将使用clone()进行实习。

在继续进行之前,要了解的主要重要事项是我们需要进程的结构。
让我们想象一下,我们需要存储有关员工的详细信息。
最简单的方法是创建变量并为其分配值。
例如,我们可以使用empname1 =“ sam”,empage1 = 30,emplocation1 =“ california”等变量。
随着我们变得越来越大,这将变得更加混乱。
想象一下100名员工(我们将需要多少个变量,并且所有变量都有一个添加的数字。
这很有趣)。

更好的方法是创建一个具有名称,年龄,位置等多个特征的名为employee的结构。
当我们需要添加员工时,我们仅使用employee结构

现在我们已经知道,进程也应该具有某种结构(因为系统中将运行数百个进程)。
Linux中的每个进程都是使用C中称为task_struct的数据结构创建的。

不要在这里对术语“任务”感到困惑。
从内核的角度来看,任务不过是一个进程。
术语“任务”和“进程”是相同的。

内核(操作系统)以列表(称为任务列表..它实际上是循环链接列表)的形式具有系统上当前正在运行的进程的全部细节。
我们现在可以猜测此列表的元素/项目。
这些项目是描述每个进程的task_struct结构。

那么,task_struct在进程方面包含哪些细节?
它包含有关进程的许多信息。
其中一些在下面。
顺便说一句,既然我们知道每个进程都有一个叫做task_struct的东西,我们可以如下定义一个进程。

进程不过是task_struct数据结构的实例,它描述了进程及其所有详细信息。

task_struct数据结构的字段描述
state当我们谈论进程时,我们可以轻松猜测这一个。进程可以在不同的状态。即中断,不间断,僵尸,停止等
Ptrace这与调试进程有关。跟踪由进程执行的系统调用。
static_prio指定进程优先级的很好值
cpus_allowed程序员可以指定CPU核心,在那里他希望运行进程或者允许运行的CPU核心 - 这是CPU亲和力所设置的地方
的 ptrace_children在追踪的下面的子进程(实际上这又一再指向另一个结构 - 另一个结构)
ptrace_list正在进行此进程的其他父进程(另一个结构)
mm如我们知道Task_struct字段可以指向其他结构(即:类似于task_struct),这个mm字段指向另一个名为mm_struct的结构。这是特定进程的存储器描述。基本上分配给进程的物理内存页面和内存地址空间
的 exit_code,exit_signal当进程退出时,这些字段用于保持信号和退出代码。这将让父进程知道孩子如何死亡
pdeath_signal当父母模具时发送的信号
PID进程标识符
父母,孩子们这是另一个指向父进程的结构,以及此进程的任何子女
utime,stime,cutime,cstime用户时间,系统时间,通过进程及其儿童花费的集体用户时间,进程和儿童的总系统时间
的 GID,UID,环境组ID,用户ID和环境变量可用于此进程
文件打开文件信息 - 再次另一个结构
信号与某些信号相关的处理程序,待处理的信号等
tgid线程组标识符

如何创建进程?

Linux中的进程创建涉及两个系统调用。
第一个创建调用进程的克隆。
让我们想象一下,我们将在Linux中执行命令“ date”。
调用进程当然是bash shell。
将进程创建为Shell进程的克隆的第一个系统调用称为clone()。
无论调用哪个库函数,都会调用clone()创建一个子进程。
创建子进程后,我们需要用date命令二进制文件替换该进程中的可执行文件。
这是通过使用execve()系统调用来实现的。

通过执行以下命令,我们几乎可以看到这种情况。

ubuntu@localhost:~$strace -f -etrace=execve,clone bash -c '{ date; }'
execve("/bin/bash", ["bash", "-c", "{ date; }"], [/* 21 vars */]) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f531bc639d0) = 19163
strace: Process 19163 attached
[pid 19163] execve("/bin/date", ["date"], [/* 21 vars */]) = 0
Wed Apr 25 08:25:49 UTC 2016
[pid 19163] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=19163, si_uid=1000, si_status=0, si_utime=0, si_stime=0} --
+++ exited with 0 +++

子进程通常是父进程的副本。
话虽这么说,不同的是PID号PPID(父进程ID),它将被设置为调用者pid(在本例中为bash进程ID)。
子进程也将有一个新的挂起信号(它不会继承父进程的挂起信号。
试想一下,如果已经有多个信号要执行给父进程ID,那该子进程还没有执行,那么当克隆发生时,子进程就不会获得这些信号)。

execve()系统调用将子进程替换为可执行程序(在我们的示例中,date命令具有绝对路径。
请参见上面的strace输出)。
这将用新的date命令替换地址空间。
一旦执行发生,我们新进程的复制地址空间就会被丢弃。

请记住,Linux中的每个进程都属于一棵树。
人们称此树为进程树。
我们可以通过在Linux中执行pstree命令来查看它。
每个进程都将有一个父进程和0个或者多个子进程(并且所有进程都从bigdaddy INIT的pid 1开始)。

从上面显示的strace命令输出中我们了解到,每个孩子都是通过克隆父对象出生的,然后替换二进制文件以使用execve()调用执行(这将丢弃复制的地址空间)。

让我们想象一下不需要execve()的情况。
我们只需要fork()调用即可,该调用在内部使用clone()来创建一个新的子进程,这恰好是父进程的副本。

在某些情况下,我们不需要execve()。
例如,让我们imgine我们需要大量的工作进程来并行执行某件事。
可能是为了接受传入的TCP连接到应用程序?

在这些情况下,我们实际上不需要execve()。
因为我们不会替换可执行二进制文件。
我们只需要再执行一个进程即可并行执行操作。
尽管我们说子进程是具有相同地址空间的父进程的副本,但实际上它并不复制地址空间。
仅在孩子要执行一些写操作的情况下才复制(否则,如果在副本上未进行任何修改,为什么我们甚至需要副本)

仅在孩子尝试更改某些内容时才复制它。
这样,可以节省复制地址空间所花费的时间和资源。
这称为写时复制方法。
通常将其称为COW。

现在我们知道了Linux为什么使用两个调用(clone和execv)的组合来实现进程的创建。

如果我们有一个应用程序一次只能做一件事,那真的很糟糕。
你不这样认为吗?
例如,设想一个Web浏览器一次只能浏览一个页面,或者一个应用程序一次只能读取键盘输入,而没有其他任何东西。
在当今世界,这是不可想象的。
这是线程介入的地方。

一个进程可以有多个线程。
意味着线程将成为进程的一部分(同一进程的所有线程将共享相同的PID)。

好吧,如果我们希望同时发生多个事情,则可以使用多个进程来实现。
为什么要穿线?
这是因为从应用程序的角度考虑时,进程之间的通信并不是那么简单。
在进程之间共享数据(在Linux中,我们具有管道,套接字或者套接字等用于在进程之间共享事物)涉及一些开销,并且相对较慢。
与进程之间的切换相比,线程之间的上下文切换更快。

内核会及时地在任务之间切换。
这是为了在系统上的所有任务(进程)之间公平地共享CPU。
与线程相比,切换进程涉及更多的开销。
这是因为线程始终位于共享地址空间上,因此当处理器将执行上下文从一个线程更改为另一个线程时,要替换的东西就更少了。

在Linux中如何创建线程?

还记得我们了解到,如今很少使用fork()系统调用,而是使用clone()创建进程吗?
相同的clone()用于在Linux中创建线程。
是的。
重要的是传递给clone()系统调用的参数。

如果克隆系统调用具有以下参数,则它将创建类似于线程的内容,而不是子进程。

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

上面作为克隆函数的参数所看到的内容指定了在创建新进程/线程时需要共享的内容。

实际上,上面的clone()系统调用将创建一个子进程,其中将共享地址空间(CLONE_VM),文件系统信息(CLONE_FS),打开的文件(CLONE_FILES)和未决的信号(CLONE_SIGHAND)。
这就是为什么我之前提到过clone()系统调用本质上是通用的。
创建新进程时,我们可以共享任何内容。
实际上,由于Linux中克隆系统调用的灵活性,我们甚至可以创建不是进程甚至线程的东西。

POSIX(便携式操作系统接口)是管理操作系统的一组标准(基于UNIX)。
需要有一个互操作性标准。
对于编程接口,shell以及操作系统中的几乎所有内容,都有一个标准。
同样,线程也有一个标准。
Linux中的初始线程实现未完全符合POSIX标准。

Linux初始线程实现不符合POSIX的主要原因有很多。
一个主要原因是PID。
线程的早期实现具有不同的PID编号(类似于进程具有不同的PID的方式)。

为了解决线程实现中的这些问题,Red Hat启动了一个称为NPTL(Native Posix线程库)的东西,后来它包含在Linux内核中(从2.6版本的内核开始)。
GNU C库中还包含用于创建线程的库函数。
该库函数称为pthread_create。

尽管可以使用克隆系统调用来创建线程,但是建议使用pthread_create。
这是出于可移植性的原因(Unix的变体不必具有clone()系统调用。
但是,pthread_create库仍然可以使用,因为它将处理基础的系统调用和其他复杂性)。

每个线程都有一个task_struct,类似于进程。
因此内核将调度这些类似于进程的进程(当然,与进程之间的切换相比,线程之间的切换是事实)。

pthread_create在内部仅使用clone()系统调用。

如何查看与Linux中的进程相关联的线程?

在ps命令手册页中,我们将看到类似于以下线程的内容。

To get info about threads:
          ps -eLf
          ps axms

因此,让我们尝试一下。
在我的一台服务器上,我有一个运行有多个线程的mongodb服务器,这应该是一个很好的例子。
见下文。

[root@localhost ~]# ps -efL |grep mongo
root      1470     1  1470  0   19 11:25 ?        00:00:11 /opt/mongodb-linux-x86_64-rhel62-3.0.4/bin/mongod --bind_ip 10.12.1.132 --dbpath /mnt/mongodb_data --fork --logpath /mnt/mongodb.log
root      1470     1  1471  0   19 11:25 ?        00:00:00 /opt/mongodb-linux-x86_64-rhel62-3.0.4/bin/mongod --bind_ip 10.12.1.132 --dbpath /mnt/mongodb_data --fork --logpath /mnt/mongodb.log
root      1470     1  1472  0   19 11:25 ?        00:00:00 /opt/mongodb-linux-x86_64-rhel62-3.0.4/bin/mongod --bind_ip 10.12.1.132 --dbpath /mnt/mongodb_data --fork --logpath /mnt/mongodb.log
root      1470     1  1473  0   19 11:25 ?        00:00:00 /opt/mongodb-linux-x86_64-rhel62-3.0.4/bin/mongod --bind_ip 10.12.1.132 --dbpath /mnt/mongodb_data --fork --logpath /mnt/mongodb.log
root      1470     1  1474  0   19 11:25 ?        00:00:06 /opt/mongodb-linux-x86_64-rhel62-3.0.4/bin/mongod --bind_ip 10.12.1.132 --dbpath /mnt/mongodb_data --fork --logpath /mnt/mongodb.log

从上面的输出中,我们可以看到所有这些进程都具有相同的PID号(1470)。
但是,它们具有唯一的编号线程ID号(1470、1471、1472、1473、1474)。

在Linux中,这些线程ID号由LWP表示(ps命令列名称也为LWP)。
LWP代表轻量进程。

实际上...在Linux中,每个程序至少都有一个线程。

[root@localhost ~]# ps -efL
UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
root         1     0     1  0    1 09:19 ?        00:00:01 /sbin/init
root         2     0     2  0    1 09:19 ?        00:00:00 [kthreadd]
root         3     2     3  0    1 09:19 ?        00:00:00 [migration/0]
root         4     2     4  0    1 09:19 ?        00:00:00 [ksoftirqd/0]
root         5     2     5  0    1 09:19 ?        00:00:00 [stopper/0]
root         6     2     6  0    1 09:19 ?        00:00:00 [watchdog/0]
root         7     2     7  0    1 09:19 ?        00:00:00 [migration/1]
root         8     2     8  0    1 09:19 ?        00:00:00 [stopper/1]
root         9     2     9  0    1 09:19 ?        00:00:00 [ksoftirqd/1]
root        10     2    10  0    1 09:19 ?        00:00:00 [watchdog/1]
root        11     2    11  0    1 09:19 ?        00:00:00 [events/0]
root        12     2    12  0    1 09:19 ?        00:00:00 [events/1]

在单线程程序中,LWP编号和PID编号始终相同。
大多数情况下会发生一个线程,一个进程。

引入TGID或者线程组标识符是为了在Linux中实现POSIX兼容线程。
线程组标识符通常是主进程的PID编号。

如果一个进程有4个线程,则所有这些线程的task_struct都将TGID设置为主进程id的PID(或者称其为第一个线程)。