线程 和进程

第一节:电脑包含了哪些东西?

第二节:一个软件的运行涉及哪些方面呢?

第三节:怎样才能立项呢?

第四节:立项服务与进程管理

操作系统进程创建过程

进程状态

进程内存管理

进程间通信

网络通信

异常处理和信号处理

第四节:线程

为什么要有线程?

如何创建线程?

线程的内存

线程数据的保护

第一节:电脑包含了哪些东西?

好不容易组装完这一大堆硬件,还是不能直接用,你还需要安装一个操作系统。安装操作系统也是一件非常复杂的事,一点儿也不亚于把刚才那堆东西组装起来。这个安装过程可能会涉及十几个步骤、几十项配置。每一步骤配置完了,点击下一步,会出现个进度条。伴随着一堆难以理解的描述,最终安装步骤到达百分之百,才出现你熟悉的那个界面。

冯诺依曼计算机的组成

第二节:一个软件的运行涉及哪些方面呢?

例如,我们点击一个QQ程序,大概会进行哪些动作?

1、 鼠标双击会触发一个中断,这相当于客户告知客户对接员“有了新需求,需要处理一下”。你会事先把处理这种问题的方法教给客户对接员。

在操作系统里面就是调用中断处理函数。操作系统发现双击的是一个图标,就明白了用户的原始诉求,准备运行 QQ 和别人聊天。

2、 运行 QQ 是一件大事,因为将来的一段时间,用户要一直和 QQ 进行交互。

这就相当于你们公司接了一个大单,而不是处理零星的客户需求,这个时候应该单独立项。一旦立了项,以后与这个项目有关的事情,都由这个项目组来处理。

3、 对 QQ 这个程序来说,它能做哪些事情,每个事情怎么做,先做啥后做啥,都已经作为程序逻辑写在程序里面,并且编译成为二进制了。这个程序就相当于项目执行计划书。

立项可不能随便立,一定要有一个项目执行计划书,说明这个项目打算怎么做,一步一步如何执行,遇到什么情况应该怎么办等等。换句话说,

4、 电脑上的程序有很多,什么有道云笔记的程序、Word 程序等等,它们都以二进制文件的形式保存在硬盘上。硬盘是个物理设备,要按照规定格式化成为文件系统,才能存放这些程序。文件系统需要一个系统进行统一管理,称为文件管理子系统(File Management Subsystem)。

5、 当你从资料库里面拿到这个项目执行计划书,接下来就需要开始执行这个项目了。项目执行计划书是静态的,项目的执行是动态的。

同理,当操作系统拿到 QQ 的二进制执行文件的时候,就可以运行这个文件了。QQ 的二进制文件是静态的,称为程序(Program),而运行起来的 QQ,是不断进行的,称为进程(Process)。

第三节:怎样才能立项呢?

你会发现,一个项目要想顺畅进行,需要用到公司的各种资源,比如说盖个公章、开个证明、申请个会议室、打印个材料等等。这里有个两难的权衡,

一方面,资源毕竟是有限的,甚至是涉及机密的,不能由项目组滥取滥用;

另一方面,就是效率,咱是一个私营企业,保证项目申请资源的时候只跑一次,这样才能比较高效。

为了平衡这一点,一方面涉及核心权限的资源,还是应该被公司严格把控,审批了才能用;

另外一方面,为了提高效率,最好有个统一的办事大厅,明文列出提供哪些服务,谁需要可以来申请,然后就会有回应。

在操作系统中,也有同样的问题,

例如多个进程都要往打印机上打印文件,如果随便乱打印进程,就会出现同样一张纸,第一行是 A 进程输出的文字,第二行是 B 进程输出的文字,全乱套了。所以,打印机的直接操作是放在操作系统内核里面的,进程不能随便操作。

但是操作系统也提供一个办事大厅,也就是系统调用(System Call)。

系统调用也能列出来提供哪些接口可以调用,进程有需要的时候就可以去调用。

这其中,立项是办事大厅提供的关键服务之一。同样,任何一个程序要想运行起来,就需要调用系统调用,创建进程。

一旦项目正式立项,就要开始执行,就要成立项目组,将开发人员分配到这个项目组,按照项目执行计划书一步一步执行。

为了管理这个项目,我们还需要一个项目经理、一套项目管理流程、一个项目管理系统,

例如程序员比较熟悉的 Jira。如果项目多,可能一个开发人员需要同时执行多个项目,这就要考验项目经理的调度能力了。

在操作系统中,进程的执行也需要分配 CPU 进行执行,也就是按照程序里面的二进制代码一行一行地执行。于是,为了管理进程,我们还需要一个进程管理子系统(Process Management Subsystem)。

如果运行的进程很多,则一个 CPU 会并发运行多个进程,也就需要 CPU 的调度能力了。

每个项目都有自己的私密资料,这些资料不能被其他项目组看到。这些资料主要是项目在执行的过程中,产生的很多中间成果,例如架构图、流程图。

执行过程中,难免要在白板上或者本子上写写画画,如果不同项目的办公空间不隔离,一方面,项目的私密性不能得到保证,A 项目的细节,B 项目也能看到;另一方面,项目之间会相互干扰,A 项目组的人刚在白板上画了一个架构图,出去上个厕所,结果 B 项目组的人就给擦了。

如果把不同的项目组分配到不同的会议室,就解决了这个问题。当然会议室是有限的,需要有人管理和分配,并且需要一个会议室管理系统。

在操作系统中,不同的进程有不同的内存空间,但是整个电脑内存就这么点儿,所以需要统一的管理和分配,这就需要内存管理子系统(Memory Management Subsystem)。

上面大概举例了一个应用运行的过程,涉及的操作系统流程,这块看下大家是否有需要讨论的地方?

第四节:立项服务与进程管理

首先,我们得有个项目,那就要有立项服务。对应到 Linux 操作系统中就是创建进程。

操作系统进程创建过程

一个项目的执行是很复杂的,需要涉及公司各个部门的工作,比如说,项目管理部门需要给这个项目组开好 Jira 和 Wiki,会议室管理部要为这个项目分配会议室等等。

所以,我们现在有两种方式,

1、 一种是列一个清单,清单里面写明每个新项目组都要开哪些账号。但是,这样每次有项目,都要重新配置一遍新的 Jira、Wiki,复杂得很。

2、 另一种方式就是咱们程序员常用的方式,CTRL/C + CTRL/V。也就是说,如果想为新项目建立一套 Jira,但是觉一个个填 Jira 里面的选项太麻烦,那就可以拷贝一个别人的,然后根据新项目的实际情况,将相应的配置改改。

总结一下

1、

在操作系统中,每个进程都有自己的内存,互相之间不干扰,有独立的进程内存空间。

2、

在 Linux 里,要创建一个新的进程,需要一个老的进程调用 fork 来实现,其中老的进程叫作父进程(Parent Process),新的进程叫作子进程(Child Process)。

3、

Linux 就是这样想的。当父进程调用 fork 创建进程的时候,子进程将各个子系统为父进程创建的数据结构也全部拷贝了一份,甚至连程序代码也是拷贝过来的。按理说,如果不进行特殊的处理,父进程和子进程都按相同的程序代码进行下去,这样就没有意义了。

所以,我们往往会这样处理:

1、 对于 fork 系统调用的返回值,如果当前进程是子进程,就返回 0;

2、 如果当前进程是父进程,就返回子进程的进程号。这样首先在返回值这里就有了一个区分,然后通过 if-else 语句判断,如果是父进程,还接着做原来应该做的事情;如果是子进程,需要请求另一个系统调用execve来执行另一个程序,这个时候,子进程和父进程就彻底分道扬镳了,也即产生了一个分支(fork)了。

进程状态

TASK_RUNNING 并不是说进程正在运行,而是表示进程在时刻准备运行的状态。这个时候,要看 CPU 小伙伴有没有空,有空就运行他,没空就得等着。

3、有时候,进程运行到一半,需要等待某个条件才能运行下去,这个时候只能睡眠。睡眠状态有两种。

一种是 TASK_INTERRUPTIBLE,可中断的睡眠状态。这是一种浅睡眠的状态,也就是说,虽然在睡眠,等条件成熟,进程可以被唤醒。

另一种睡眠是 TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。这是一种深度睡眠状态,不可被唤醒,只能死等条件满足。

4、 有了一种新的进程睡眠状态,TASK_KILLABLE,可以终止的新睡眠状态。进程处于这种状态中,他的运行原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号,也即虽然在深度睡眠,但是可以被干掉。

5、 一旦一个进程要结束,先进入的是 EXIT_ZOMBIE 状态,但是这个时候他的父进程还没有使用 wait() 等系统调用来获知他的终止信息,此时进程就成了僵尸进程。

6、 EXIT_DEAD 是进程的最终状态。

例子

系统启动之后,init 进程会启动很多的 daemon 进程,为系统运行提供服务,然后就是启动 getty,让用户登录,登录后运行 shell,用户启动的进程都是通过 shell 运行的,从而形成了一棵进程树。

我们可以通过 ps -ef 命令查看当前系统启动的进程,我们会发现有三类进程。

你会发现,PID 1 的进程就是我们的 init 进程 systemd,PID 2 的进程是内核线程 kthreadd,这两个我们在内核启动的时候都见过。其中用户态的不带中括号,内核态的带中括号。

接下来进程号依次增大,但是你会看所有带中括号的内核态的进程,祖先都是 2 号进程。而用户态的进程,祖先都是 1 号进程。tty 那一列,是问号的,说明不是前台启动的,一般都是后台的服务。

pts 的父进程是 sshd,bash 的父进程是 pts,ps -ef 这个命令的父进程是 bash。这样整个链条都比较清晰了。

进程内存管理

 

 项目启动之后,每个项目组有独立的会议室,存放自己项目相关的数据。每个项目组都感觉自己有独立的办公空间。

在操作系统中,每个进程都有自己的内存,互相之间不干扰,有独立的进程内存空间。

项目执行的过程中,会产生一些架构图、流程图,这些也放在会议室里面。有的画在白板上,讨论完了,进入下个主题就会擦了;有的画在纸和本子上,讨论的时候翻出来,不讨论的时候堆在那里,会保留比较长的一段时间,除非指明的确不需要了才会去销毁。

对于进程的内存空间来讲,放进程运行中产生数据的这部分,我们称为数据段(Data Segment)。其中局部变量的部分,在当前函数执行的时候起作用,当进入另一个函数时,这个变量就释放了;

也有动态分配的,会较长时间保存,指明才销毁的,这部分称为堆(Heap)。

当分配的内存数量比较小的时候,使用 brk,会和原来的堆的数据连在一起,这就像多分配两三个工位,在原来的区域旁边搬两把椅子就行了。当分配的内存数量比较大的时候,使用 mmap,会重新划分一块区域,也就是说,当办公空间需要太多的时候,索性来个一整块。

进程间通信

当某个项目比较大的时候,可能分成多个项目组,不同的项目组需要相互交流、相互配合才能完成,这就需要一个项目组之间的沟通机制。项目组之间的沟通方式有很多种,我们来一一规划。

1、 首先就是发个消息,不需要一段很长的数据,这种方式称为消息队列(Message Queue)。由于一个公司内的多个项目组沟通时,这个消息队列是在内核里的,我们可以通过msgget创建一个新的队列,msgsnd将消息发送到消息队列,而消息接收方可以使用msgrcv从队列中取消息。

2、 两个项目组需要交互的信息比较大的时候,可以使用共享内存的方式,也即两个项目组共享一个会议室(这样数据就不需要拷贝来拷贝去)。大家都到这个会议室来,就可以完成沟通了。这时候,我们可以通过shmget创建一个共享内存块,通过shmat将共享内存映射到自己的内存空间,然后就可以读写了。

3、个项目组共同访问一个会议室里的数据,就会存在“竞争”的问题。

如果大家同时修改同一块数据咋办?这就需要有一种方式,让不同的人能够排他地访问,这就是信号量的机制Semaphore。

管道(PIPE)

FIFO(有名管道)

XSI消息队列

XSI信号量

XSI共享内存

POSIX信号量

域套接字(Domain Socket)

信号(Signal)

互斥量(Mutex)

网络通信

同一个公司不同项目组之间的合作搞定了,如果是不同公司之间呢?也就是说,这台 Linux 要和另一台 Linux 交流,这时候,我们就需要用到网络服务。

不同机器的通过网络相互通信,要遵循相同的网络协议,也即TCP/IP 网络协议栈。Linux 内核里有对于网络协议栈的实现。如何暴露出服务给项目组使用呢?

网络服务是通过套接字 Socket 来提供服务的。Socket 这个名字很有意思,可以作“插口”或者“插槽”讲。虽然我们是写软件程序,但是你可以想象成弄一根网线,一头插在客户端,一头插在服务端,然后进行通信。因此,在通信之前,双方都要建立一个 Socket。

异常处理和信号处理

在项目运行过程中,不一定都是一帆风顺的,很可能遇到各种异常情况。作为老板,处理异常情况的能力是非常重要的,所以办事大厅也一定要包含这部分服务。

当项目遇到异常情况,例如项目中断,做到一半不做了。这时候就需要发送一个信号(Signal)给项目组。经常遇到的信号有以下几种:

  • 在执行一个程序的时候,在键盘输入“CTRL+C”,这就是中断的信号,正在执行的命令就会中止退出;
  • 如果非法访问内存,例如你跑到别人的会议室,可能会看到不该看的东西;
  • 硬件故障,设备出了问题,当然要通知项目组;
  • 用户进程通过kill函数,将一个用户信号发送给另一个进程。

当项目组收到信号的时候,项目组需要决定如何处理这些异常情况。

对于一些不严重的信号,可以忽略,该干啥干啥,但是像 SIGKILL(用于终止一个进程的信号)和 SIGSTOP(用于中止一个进程的信号)是不能忽略的,可以执行对于该信号的默认动作。每种信号都定义了默认的动作,例如硬件故障,默认终止;也可以提供信号处理函数,可以通过sigaction系统调用,注册一个信号处理函数。

提供了信号处理服务,项目执行过程中一旦有变动,就可以及时处理了。

总结一下:

第四节:线程

为什么要有线程?

其实,对于任何一个进程来讲,即便我们没有主动去创建线程,进程也是默认有一个主线程的。线程是负责执行二进制指令的,它会根据项目执行计划书,一行一行执行下去。进程要比线程管的宽多了,除了执行指令之外,内存、文件系统等等都要它来管。

所以,进程相当于一个项目,而线程就是为了完成项目需求,而建立的一个个开发任务。默认情况下,你可以建一个大的任务,就是完成某某功能,然后交给一个人让它从头做到尾,这就是主线程。

但是有时候,你发现任务是可以拆解的,如果相关性没有非常大前后关联关系,就可以并行执行。

例子:

例如,你接到了一个开发任务,要开发 200 个页面,最后组成一个网站。为了完成这个大的项目(进程),就不能一个人从头干到尾了,这样肯定赶不上工期。于是,周瑜将一个大项目拆分成 20 个子项目,每个子项目完成 10 个页面,一个大项目组也分成 20 个小组,并行开发,都开发完了,再做一次整合,这肯定比依次开发 200 个页面快多了。如果项目叫进程,那子项目就叫线程。

那我们能不能成立多个项目组实现并行开发呢?当然可以了,只不过这样做有两个比较麻烦的地方。

第一个麻烦是,立项。涉及的部门比较多,总是劳师动众。你本来想的是,只要能并行执行任务就可以,不需要把会议室都搞成独立的。另一个麻烦是,项目组是独立的,会议室是独立的,很多事情就不受你控制了,例如一旦有了两个项目组,就会有沟通问题。

所以,使用进程实现并行执行的问题也有两个。

第一,创建进程占用资源太多;第二,进程之间的通信需要数据在不同的内存空间传来传去,无法共享。

如何创建线程?

在 Linux 里面,无论是进程,还是线程,到了内核里面,我们统一都叫任务,由一个统一的结构 task_struct 进行管理。

创建一个线程调用的是 pthread_create

1、 线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。pthread_create 不是一个系统调用,是 Glibc 库的一个函数,所以我们还要去 Glibc 里面去找线索。

2、 在内核里一样,每一个进程或者线程都有一个 task_struct 结构,在用户态也有一个用于维护线程的结构,就是这个 pthread 结构。

3、 凡是涉及函数的调用,都要使用到栈。每个线程也有自己的栈

4、 线程栈是在进程的堆里面创建的

这里不深入讲进程和线程在linux系统里面的实现,比较复杂

线程的内存

线程可以将项目并行起来,加快进度,但是也带来的负面影响,过程并行起来了,那数据呢?

线程访问的数据细分成三类:

1、 第一类是线程栈上的本地数据,比如函数执行过程中的局部变量。前面我们说过,函数的调用会使用栈的模型,这在线程里面是一样的。只不过每个线程都有自己的栈空间。

栈的大小可以通过命令 ulimit -a 查看,默认情况下线程栈大小为 8192(8MB)

2、 第二类数据就是在整个进程里共享的全局数据

例如全局变量,虽然在不同进程中是隔离的,但是在一个进程中是共享的。如果同一个全局变量,两个线程一起修改,那肯定会有问题,有可能把数据改的面目全非。这就需要有一种机制来保护他们,比如你先用我再用。

3、第三类数据,线程私有数据(Thread Specific Data)

线程数据的保护

1、Mutex,全称 Mutual Exclusion,中文叫互斥

它的模式就是在共享数据访问的时候,去申请加把锁,谁先拿到锁,谁就拿到了访问权限,其他人就只好在门外等着,等这个人访问结束,把锁打开,其他人再去争夺,还是遵循谁先拿到谁访问。

2、

互斥量

条件变量

读写锁

自旋锁

barrier

很多内容参考了极客时间的学习资料,希望大家可以去购买专栏的课程趣谈Linux操作系统_Linux_操作系统-极客时间

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值