Linux中进程和线程之间的区别
我们经常听到人们经常使用两个词。
一个是“进程”,另一个是“线程”。
哪个是进程,哪个是线程,以及区分这两者的方法通常会使许多人感到困惑。
在本文中,我们将尝试在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(或者称其为第一个线程)。