进程和线程

1. 线程和进程的区别?

  • 进程是资源分配的最小单元;线程是程序执行的最小单元,也是处理器调度的基本单元。
  • 进程有独立的地址空间,启动一个进程系统会为它分配地址空间,建立数据表来维护代码段,堆栈段和数据段;线程共享进程中的数据,使用相同的地址空间。
  • 进程通信需要以IPC的方式(inter-Process Communication,进程间通信);线程共享全局变量,静态变量等数据,通信更为方便。
  • 进程切换消耗资源大,效率低,但多进程程序更健壮,一个进程死掉并不会对另外一个进程造成影响;多线程程序只要有一个线程死掉,整个进程也跟着死掉了。

记忆点:最小单元、地址空间、通信方式、健壮性

2. 进程间通信的方式?

进程间通信方式包括:管道(无名管道、有名管道、高级管道)、消息队列、共享内存、信号、信号量、套接字。

像我这种以前一直从事裸机开发的人来说有个疑问,为什么需要这么多通信方式呢,不能直接通过内存来访问吗?这么多进程间的通信由于地址空间原因,每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。

3.每种通信方式的优缺点以及应用场景

1. 管道:

 优缺点:管道这种通信方式效率低,不适合进程间频繁地交换数据。好处就是简单,同时很容易知道管道里的数据已经被另一个进程读取了。

        应用场景无名管道,无名管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。有名管道,有名管道也是半双工的通信方式,但是它允许无亲缘进程间通信。高级管道,将另一个程序当做一个新的进程在当前程序中启动,则它算是当前程序的子进程。

        分析无名管道的创建,需要通过下面的系统调用:int pipe(int fd[2])

这里表示创建一个匿名管道,并返回了两个描述符,一个管道的读取描述符fd[0],另一个是管道的写入端描述符fd[1]。注意,这个匿名管道是特殊的文件,只存在于内存,不存在于文件系统中。

 

 其实,所谓的管道,就是内核里面的一串缓存。从管道的一端写入的数据,实际上是缓存在内核中的。另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。但是由于这两个描述符都是在一个进程里面,并没有起到进程间通信的作用,那么怎么才能使得管道跨进程通信呢?

我们可以使用fork创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个fd[0]和fd[1],两个今晨就可以通过各自的fd写入和读取同一个管道文件实现跨进程通信了。

这里有个问题,管道只能一端写入另一端读出,上面这种容易造成混乱,因为父进程和子进程都可以写入和读出。为了避免这种情况,通常的做法是:

父进程关闭读取的fd[0],只保留写入的fd[1];    子进程关闭写入的fd[1],只保留读取的fd[1];

如果需要双向通信,则需要创建两个管道。

管道2 父进程关闭写入的fd[1],只保留读取的fd[0];    子进程关闭读取的fd[0],只保留写入的fd[1];到这里我们仅仅解析了使用管道进行父进程和子进程之间的通信,但是我们shell里面并不是这样的。

在shell里面, A | B 命令的时候,A进程和B进程都是shell创建出来的子进程,A和B之间不存在父子关系,它俩的父进程都是shell。

 所以说,在shell里面通过 | 无名管道将多个命令连接在一起,实际上就是创建了多个子进程,那么在编写shell脚本时,能使用一个管道搞定的事情,就不要多用一个管道,这样可以减少创建子进程的系统开销。

总结:对于无名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有挂到文件,只能通过fork来复制父进程fd文件描述符,来达到通信的目的。

另外,对于命名管道,她可以在不相关的进程间也能相互通信。因为命名管道,提前创建了一个类型为管道的设备文件,在进程中只要使用这个设备文件,就可以互相通信。

不管是无名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数都遵循先进先出原则,不支持lseek之类的文件定位操作。

2. 消息队列

消息队列的提出是为了解决管道不适合频繁地交换数据的问题。

那么消息队列是如何解决这一问题的呢? 比如,A进程要给B进程发送消息,A进程把数据放在对应的消息队列就可以正常返回了,B进程需要的时候再去读取数据就可以。同理,B进程要给A进程发送消息也是如此。

再来,消息队列是保存在内核中的消息链表,在发送数据的时候,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列总读取了消息体,内核就会把这个消息体删除。

消息队列的生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。

消息这种模型,就像发邮件一样,你来一封,我回一封,可以频繁沟通了。

消息队列也有不足的地方,一是通信不及时,二是附件也有大小限制

消息度列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在linux内核中,会有两个宏定义MSGMAX和MSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。

消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据都内核态的过程,同理另一个进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

3. 共享内存

共享内存的提出是为了解决消息队列在读取和写入的过程时,存在资源和时间消耗的问题,因为都会发生用户态和内核态之间的拷贝过程。

现在操作系统,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程A和进程B的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删改查互不影响。

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到,都不需要拷贝来拷贝去,大大提高了进程间通信的速度。

 

4. 信号量

一种方式的提出总是会引起另外的问题,没有一种方式是完美的。信号量的提出是为了解决多个进程同时修改同一个共享内存,很有可能就冲突。例如,两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。

为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。

信号量本质是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。

信号量表示资源的数量,控制信号量的方式有两种原子操作:

  • 一个是P操作,这个操作会把信号量减去1,相减后如果信号量<0, 则表明资源已被占用,进程需阻塞等待;相减后如果信号量>=0,则表明还有资源可使用,进程可正常继续执行。
  • 另外一个操作是V操作,这个操作会把信号量加上1,相加后如果信号量<=0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后果信号量>0, 则表明当前没有阻塞的进程。

 p操作是用在进入共享内存之前,V操作是用在离开共享内存之后,这两个操作是必须成对出现的

举例说明:

初始化信号量为1,来实现两个进程互斥访问共享内存。-互斥信号量

具体过程如下:

  • 进程A在访问共享内存前,先执行了P操作,由于信号量的初始值为1,故执行p操作之后,信号量的值变为0,表示共享资源可以正常使用,于是进程A就可以访问共享资源。
  • 这时候,进程B想要对共享资源进行访问,先执行P操作,由于进程A已经进行了P操作来访问共享资源,所以信号量的值减一变为-1,此时信号量<0,表示资源已被其他线程占用,需要阻塞等待资源释放。
  • 当进程A访问完共享资源,才会执行V操作,使得信号量变为0,接着就会唤醒阻塞中的线程B,使得线程B可以访问共享资源,最后线程B完成共享内存的访问后,执行V操作,使信号量恢复到初始值1

 另外,在多进程中,每个进程并不一定是顺序执行的,他们基本是以各自独立的、不可预知的速度向前推进,但有时候我们多希望多个进程能密切合作,以实现一个共同的任务。

例如,进程A是负责生产数据,而进程B是负责读取数据,这两个进程是相互合作、依赖的。进程A必须先生产了数据,进程B才能读取到数据,所以执行时有前后顺序的。

初始化信号量为0,来实现多进程同步。-同步信号量

具体过程如下:

  • 如果进程B比进程A先执行了,那么执行到P操作时,由于信号量初始值是0,故信号量会变成-1,表示进程A还没有生产数据,于是进程B就阻塞等待。
  • 接着,当进程A生产完数据后,执行V操作,就会使得信号量变为0,于是就会唤醒阻塞在P操作的进程B
  • 最后,进程B被唤醒后,意味着进程A已经生产了数据,于是数据B就可以正常读取数据了。

5. 信号

信号的提出是解决在异常情况下的工作模式。上面提到的都是在常规状态的工作模式。

信号和信号量的用途完全不一样。在linux操作系统中,为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过kill -l 命令,查看所有的信号:

运行在shell终端的进程,我们可以通过键盘输入默写组合键的时候,给进程发送信号。例如

  • Ctrl+C 产生SIGINT信号,表示终止该进程。
  • Ctrl+Z产生SIGTSTP信号,表示停止该进程,但还未结束。

 kill -2 中断(同Ctrl+C)中断,是程序在结束之前,能够保存相关数据,然后再推出。

kill -9 直接强制结束程序

所以,信号事件的来源主要有硬件来源(键盘ctrl+c)和软件来源(kill 命令)

信号时进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面几种,用户进程对信号的处理方式:

  • 执行默认操作。linux对每种信号都规定了默认操作。例如,上面列表的SIGTERM信号,就是终止进程的意思。Core的意思是Core Dump,也即终止进程后,通过Core Dump将当前进程的运行状态保存在文件里面,方便程序员事后进行分析问题出在那里。
  • 捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
  • 忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号应用进出无法捕捉和忽略的,即SIGKILL和SEGSTOP,它们用于在任何时候中断或结束某一进程 。

6. 套接字socket

   socket的提出是为了解决跨网络与不同主机上的进程之间通信。上面提到的管道、消息队列、共享内存、信号量和信号都是同一台主机上进行进程间通信。

实际上,Socket通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。

我们来看看创建socket的系统调用:int socket(int domain,int type,int protocal)

三个参数:

  • domain参数用来指定协议族,比如AF_INET用于IPV4,AF_INET6用于IPV6、AF_LOCAL/AF_UNIX用于本机;
  • type参数用来指定通信特性,比如SOCK_STREAM表示的是字节流、对应TCP、SOCK_DGRAM表示的是数据报,对应UDP、SOCK_RAW表示的原始套接字;
  • protocal参数原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol目前一般写成0即可。

根据创建socket类型不同,通信的方式也就不同:

  • 实现TCP字节流通信:socket类型AF_INET和SOCK_STREAM;
  • 实现UDP数据报通信:socket类型为AF_INET和SOCK_DGRAM;
  • 实现本地进程间通信:本地字节流类型是AF_LOCAL和SOCK_STREAM;本地数据报类型是AF_LOCAL和SOCK_DGRAM。另外,AF_UNIX和AF_LOCAL是等价的,所以AF_UNIX也属于本地socket;

 接下来,简单说一下三种通信的编程模式。

针对TCP协议通信的socket编程模式

  •  服务端和客户端初始化socket,得到文件描述符;
  • 服务端调用bind,将绑定在IP地址和端口;
  • 服务端调用listen,进行监听;
  • 服务端调用accpet,等待客户端连接;
  • 客户端调用connect,向服务器的地址和端口发送连接请求;
  • 服务端accept返回用于传输的socket的文件描述符;
  • 客户端用write写入数据,服务端用read读取数据;
  • 客户端断开连接,会调用close,那么服务端read读取数据的时候,就会读取到EOF,待处理完数据后,服务端用close,表示连接关闭。

这里需要注意的是,服务端调用accept时,连接成功了会返回一个已经完成连接的socket,后续用来传输数据。

所以,监听的socket和真正用来传送数据的socket,是两个socket,一个叫作监听socket,一个叫做已完成连接的socket 

成功连接建立之后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西一样。

 针对UDP协议通信的socket编程模型:

UDP是没有连接的,所以不需要三次握手,也就不需要TCP调用listen和connect,但是UDP的交互仍然需要IP地址和端口号,因此也需要bind。

对于UDP来说,不需要维护连接,那么也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念,只要有一个socket多台机器就可以任意通信,因此每一个UDP的socket都需要bind。

另外,每次通信是,调用sendto和recvfrom,都要传入目标主机的IP地址和端口。

针对本地进程间通信的socket编程模型

本地socket被用于在同一台主机上进程间通信的场景:

  • 本地socket的编程接口和IPv4、IPv6套接字编程接口是一致的,可以支持字节流和数据报两种协议;
  • 本地socket的实现效率大大高于IPv4和IPv6的字节流、数据报socket的实现;

本地字节流socket和本地数据报socket在bind的时候,不像TCP和UDP要绑定IP地址和端口,而是绑定一个本地文件,这也就是他们之前的最大区别。 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值