1. 定义
进程是执行中一段的程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
线程是每一个进程中执行的每个任务就是一个线程。是进程中的一条执行路线。线程是进程中执行运算的最小单位。线程是任务调度和执行的最小单元。
写好的a.c文件,经过gcc编译:gcc a.c,就会生成a.out文件(gcc -c 编译;gcc -o 联结),可通过 ./a.out 运行,可通过size来查看虚拟内存组织管理情况。
这么说好像有点抽象,可以先来说说之前的单核CPU为什么可以执行多任务多程序,是因为进程调度机制来实现的,每个进程都会被分配独立的用户空间(数据段、代码段、堆栈段等)和内核空间(进程上下文等),这就会引起一些开销,对于单机而言,虽然有影响,但不是那么明显,当多台主机访问服务器的时候,同时内一台服务器会面对多个请求,这个问题就显现出来了,首先,1. 切换进程,会占用很多的资源,为每个请求都创建一个进程,显然会占用大量的资源;2. 切换进程,会需要保存现场和恢复现场,这会占用很多的时间,响应延迟就会比较高。
也这是因为这两个原因,出现了线程的概念。线程是进程中的一个执行单元,它共享进程的地址空间,但是有自己的独立的栈空间,这样面对多个请求时只需要建立一个线程响应,就可以极大的节约空间。在同一个进程中的线程做切换时不需要太多的保存和恢复现场就能够实现线程的切换;只有进程间的线程做切换时才会进行进程切换,提高了效率。
推荐一个视频:https://www.56.com/u50/v_MTUwMTU1Njcx.html
2. 进程的创建
由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。
fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。可以这样想象,2个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因。
至于哪一个最先运行,可能与操作系统(调度算法)有关,而且这个问题在实际应用中并不重要,如果需要父子进程协同,可以通过原语的办法解决。
3. 进程与线程的异同点
-
一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。(进程和线程的关系:1对n,n对1)
-
线程是一种轻量级的进程,与进程相比,线程给操作系统带来侧创建、维护、和管理的负担要轻,意味着线程的代价或开销比较小。
-
线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权,线程文本包含在他的进程 的文本片段中,进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段, 寄存器的内容,栈段又叫运行时段,用来存放所有局部变量和临时变量。
-
父和子进程使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来通信。
-
进程内的任何线程都被看做是同位体,且处于相同的级别。不管是哪个线程创建了哪一个线程,进程内的任何线程都可以销毁、挂起、恢复和更改其它线程的优先权。线程也要对进程施加控制,进程中任何线程都可以通过销毁主线程来销毁进程,销毁主线程将导致该进程的销毁,对主线程的修改可能影响所有的线程。
-
子进程不对任何其他子进程施加控制,进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制,进程中所有线程都可以对主线程施加控制。
-
无论是static还是非static的全局变量,如果不加限制随意访问的话易出现同步问题。无论是static还是非static的局部变量,每个线程都是私有的,其他线程不会对其进行干扰。
在多线程中不加限制的随意访问非static局部变量可能会导致运算结果出错
在多线程中不加限制的随意访问非static全部变量可能会导致运算结果出错
在多线程中不加限制的随意访问static局部变量可能会导致运算结果出错
在多线程中不加限制的随意访问static全部变量可能会导致运算结果出错
4.进程和线程的关系
- 一个进程中有一个到多个线程。一个线程只属于一个进程。
- 一个进程中的多个线程之间是一种竞争关系。竞争的是进程独立的内存空间和系统资源。
可参考视频:https://v.qq.com/x/page/n0862wocvk7.html
5.进程间的通信机制
进程划分的原则:如果执行和操作的资源比较独立,一般建议使用进程(详细介绍在进程和线程的内容),尽量减少进程间的耦合度。
各个进程比较独立,不能直接访问对方的资源,但是进程不是孤立的,必然涉及到数据的交互,举例:一个网络音频播放器,主要涉及到两个操作:(1)网络下载;(2)音频解码和播放。一般会放在两个进程,但是网络下载的资源在哪里来供音频解码和下载,于是诞生了进程间的通信。
基本通信方式:
参见视频链接:https://www.bilibili.com/video/av48094762/?p=41,感谢万能B站。
- 为什么需要通信机制:
- 信号量及管程,只能传递很简单的信息,不能传递复杂信息
- 管程不适用多处理器情况
所以在传递大量信息时,需要新的处理机制。
- 消息传递
send & receive原语
发送进程将消息的内容和大小都准备好,但是由于在自己的地址空间进行操作,与接受进程的地址空间是相互独立的,因此发送消息这件事就交由操作系统来完成。
操作系统在它的区域里设置一组消息缓冲区,每一个消息都由一个buffer来存放。
当发送进程想要将消息发送给接受进程时,会调用发送原语send,而发送原语实际上就是陷入内核,由操作系统来完成发送工作,操作系统主要做的工作就是将发送消息拷贝到某一个空的消息缓冲区中,然后把消息挂接到接收进程的消息队列的末尾。
此时消息进程已经完成了消息的发送工作。那么对于接收进程,什么时候被CPU调度执行了,那么这个时候执行receive原语,也是陷入内核,操作系统会帮它把消息复制到接收进程的地址空间。
这样就完成了发送 -> 接收的过程。
用P、V操作实现send原语:
- 共享内存
1.需要在物理内存建立一块大家都可以访问的物理内存空间,并分别建立映射;
2.读者写者问题,可以同时读,但是不能同时写数据。
- 管道通信方式 PIPE
可以有很多进程向管道中写,有很多进程来接受消息。
- 信号量和信号
信号和信号量是不同的,它们虽然都可以用来实现同步和互斥,但是前者是使用信号处理器来进行的,后者是使用P、V操作来实现的。
- 原子操作(1)
- 屏障
参考博客及文档:
https://baijiahao.baidu.com/s?id=1611925141861592999&wfr=spider&for=pc
https://www.cnblogs.com/LUO77/p/5816326.html
https://blog.csdn.net/kuang_tian_you/article/details/86691725
只做学习交流,如果有侵权,请联系我,感谢。