「图文结合」Linux 进程、线程、文件描述符的底层原理

「图文结合」Linux 进程、线程、文件描述符的底层原理,第1张

开发十年经验总结,阿里架构师的手写Spring boot原理实践文档

阿里架构师的这份:Redis核心原理与应用实践,带你手撕Redis

Tomcat结构原理详解

说到进程,恐怕面试中最常见的问题就是线程和进程的关系了,那么先说一下答案: 在 Linux 系统中,进程和线程几乎没有区别

Linux 中的进程其实就是一个数据结构,顺带可以理解文件描述符、重定向、管道命令的底层工作原理,最后我们从 *** 作系统的角度看看为什么说线程和进程基本没有区别。

首先,抽象地来说,我们的计算机就是这个东西:

这个大的矩形表示计算机的 内存空间 ,其中的小矩形代表 进程 ,左下角的圆形表示 磁盘 ,右下角的图形表示一些 输入输出设备 ,比如鼠标键盘显示器等等。另外,注意到内存空间被划分为了两块,上半部分表示 用户空间 ,下半部分表示 内核空间

用户空间装着用户进程需要使用的资源,比如你在程序代码里开一个数组,这个数组肯定存在用户空间;内核空间存放内核进程需要加载的系统资源,这一些资源一般是不允许用户访问的。但是注意有的用户进程会共享一些内核空间的资源,比如一些动态链接库等等。

我们用 C 语言写一个 hello 程序,编译后得到一个可执行文件,在命令行运行就可以打印出一句 hello world,然后程序退出。在 *** 作系统层面,就是新建了一个进程,这个进程将我们编译出来的可执行文件读入内存空间,然后执行,最后退出。

你编译好的那个可执行程序只是一个文件,不是进程,可执行文件必须要载入内存,包装成一个进程才能真正跑起来。进程是要依靠 *** 作系统创建的,每个进程都有它的固有属性,比如进程号(PID)、进程状态、打开的文件等等,进程创建好之后,读入你的程序,你的程序才被系统执行。

那么, *** 作系统是如何创建进程的呢? 对于 *** 作系统,进程就是一个数据结构 ,我们直接来看 Linux 的源码:

task_struct 就是 Linux 内核对于一个进程的描述,也可以称为「进程描述符」。源码比较复杂,我这里就截取了一小部分比较常见的。

我们主要聊聊 mm 指针和 files 指针。 mm 指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方; files 指针指向一个数组,这个数组里装着所有该进程打开的文件的指针。

先说 files ,它是一个文件指针数组。一般来说,一个进程会从 files[0] 读取输入,将输出写入 files[1] ,将错误信息写入 files[2] 。

举个例子,以我们的角度 C 语言的 printf 函数是向命令行打印字符,但是从进程的角度来看,就是向 files[1] 写入数据;同理, scanf 函数就是进程试图从 files[0] 这个文件中读取数据。

每个进程被创建时, files 的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引 ,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。

我们可以重新画一幅图:

对于一般的计算机,输入流是键盘,输出流是显示器,错误流也是显示器,所以现在这个进程和内核连了三根线。因为硬件都是由内核管理的,我们的进程需要通过「系统调用」让内核进程访问硬件资源。

PS:不要忘了,Linux 中一切都被抽象成文件,设备也是文件,可以进行读和写。

如果我们写的程序需要其他资源,比如打开一个文件进行读写,这也很简单,进行系统调用,让内核把文件打开,这个文件就会被放到 files 的第 4 个位置,对应文件描述符 3:

明白了这个原理, 输入重定向 就很好理解了,程序想读取数据的时候就会去 files[0] 读取,所以我们只要把 files[0] 指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘:

同理, 输出重定向 就是把 files[1] 指向一个文件,那么程序的输出就不会写入到显示器,而是写入到这个文件中:

错误重定向也是一样的,就不再赘述。

管道符其实也是异曲同工,把一个进程的输出流和另一个进程的输入流接起一条「管道」,数据就在其中传递,不得不说这种设计思想真的很巧妙:

到这里,你可能也看出「Linux 中一切皆文件」设计思路的高明了,不管是设备、另一个进程、socket 套接字还是真正的文件,全部都可以读写,统一装进一个简单的 files 数组,进程通过简单的文件描述符访问相应资源,具体细节交于 *** 作系统,有效解耦,优美高效。

首先要明确的是,多进程和多线程都是并发,都可以提高处理器的利用效率,所以现在的关键是,多线程和多进程有啥区别。

为什么说 Linux 中线程和进程基本没有区别呢,因为从 Linux 内核的角度来看,并没有把线程和进程区别对待。

我们知道系统调用 fork() 可以新建一个子进程,函数 pthread() 可以新建一个线程。 但无论线程还是进程,都是用 task_struct 结构表示的,唯一的区别就是共享的数据区域不同 。

换句话说,线程看起来跟进程没有区别,只是线程的某些数据区域和其父进程是共享的,而子进程是拷贝副本,而不是共享。就比如说, mm 结构和 files 结构在线程中都是共享的,我画两张图你就明白了:

所以说,我们的多线程程序要利用锁机制,避免多个线程同时往同一区域写入数据,否则可能造成数据错乱。

那么你可能问, 既然进程和线程差不多,而且多进程数据不共享,即不存在数据错乱的问题,为什么多线程的使用比多进程普遍得多呢 ?

因为现实中数据共享的并发更普遍呀,比如十个人同时从一个账户取十元,我们希望的是这个共享账户的余额正确减少一百元,而不是希望每人获得一个账户的拷贝,每个拷贝账户减少十元。

当然,必须要说明的是, 只有 Linux 系统将线程看做共享数据的进程 ,不对其做特殊看待 ,其他的很多 *** 作系统是对线程和进程区别对待的,线程有其特有的数据结构,我个人认为不如 Linux 的这种设计简洁,增加了系统的复杂度。

在 Linux 中新建线程和进程的效率都是很高的,对于新建进程时内存区域拷贝的问题,Linux 采用了 copy-on-write 的策略优化,也就是并不真正复制父进程的内存空间,而是等到需要写 *** 作时才去复制。 所以 Linux 中新建进程和新建线程都是很迅速的

三种方法:1。 使用top命令,具体用法是 top -H 加上这个选项,top的每一行就不是显示一个进程,而是一个线程。2。 使用ps命令,具体用法是 ps -xH 这样可以查看所有存在的线程,也可以使用grep作进一步的过滤。3。 使用ps命令,具体用法是 ps -mq PID 这样可以看到指定的进程产生的线程数目。更进一步,其实一些系统监控工具,在本质上也是读取的系统产生的文件罢了。比如说进程这个事情,现在假设有进程号为 5000 的进程,查看这个进程的所有具体信息,在哪查看呢?看看这个目录吧,/proc/5000/ 这里面有你所有想要的。其实stat代表着当前的一些信息。使用ps命令来查看进程的时候,进程状态分别对应的含义如下:D 不可中断睡眠 (通常是在IO *** 作) 收到信号不唤醒和不可运行, 进程必须等待直到有中断发生R 正在运行或可运行(在运行队列排队中)S 可中断睡眠 (休眠中, 受阻, 在等待某个条件的形成或接受到信号)T 已停止的 进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行W 正在换页(2.6.内核之前有效)X 死进程 (未开启)Z 僵尸进程 进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放BSD风格的<高优先级(not nice to other users)N 低优先级(nice to other users)L 页面锁定在内存(实时和定制的IO)s 一个信息头l 多线程(使用 CLONE_THREAD,像NPTL的pthreads的那样)+ 在前台进程组

姓名:王央京    学号:18050100052   学院:电子工程学院

转自:https://blog.csdn.net/qq_22847457/article/details/89371217

【嵌牛导读】本文介绍了Linux线程的相关信息

【嵌牛鼻子】Linux线程

【嵌牛提问】在了解Linux系统后,能否具体介绍线程的概念?

【嵌牛正文】

类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。一个进程可以有多个线程,这个进程本身也叫做线程只不过是主线程。通常主线程分配任务给子线程做。程序设计时候就可以某一时刻不止做一件事情,每一个线程处理各自独立的任务。

多个线程可以访问相同的存储地址空间和文件描述符。同一进程内的线程共享以下数据:全局内存、进程指令、打开的文件、信号处理函数和信号处置、当前工作目录、用户ID和用户组ID、大多数数据。每个线程有各自的线程ID、寄存器集合(包括程序计数器和栈指针)、栈、errono、信号掩码、优先级。

线程的优点有提高程序并发性、开销小和数据通信、共享数据方便等。线程的缺点有库函数不稳定、调试编写困难、gdb不支持、对信号支持不好等。除此之外,多线程内如果其中一个线程出现了 除0、野指针 等问题会造成该线程崩溃,进而导致整个进程终止。同时,线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

从上述分析来看,线程的优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

线程有一套完整的与其有关的函数库调用,它们中的绝大多数函数名都以pthread_开头。为了使用这些函数库调用,我们必须定义宏_REENTRANT,在程序中包含头文件pthread.h,并且在编译程序时需要用选项-lpthread来链接线程库。其中常用的函数库如下:

1.  pthread_self函数获取线程ID,其作用对应进程中getpid()函数。

2.  pthread_create函数创建一个新线程,其作用对应进程中fork()函数。

3.  pthread_exit函数将单个线程退出,其作用对应进程中exit()函数

4.  pthread_join函数阻塞等待线程退出,获取线程退出状态其作用,对应进程中waitpid()函数。

5.  pthread_cancel函数杀死(取消)线程其作用,对应进程中kill()函数。

6.  pthread_detach函数实现线程分离。


欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/yw/8927746.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-23
下一篇2023-04-23

发表评论

登录后才能评论

评论列表(0条)

    保存