0%

进程那些事儿

伟大的心胸,应该表现出这样的气概——用笑脸来迎接悲惨的厄运,用百倍的勇气来应付一切的不幸。 —— 鲁迅

进程

一、基础

  1. 定义:进程是程序的一次执行过程,是系统进行资源分配和调度的一个独立单位。
  2. 一般情况下,一个进程一次只能执行一个任务。如果有很多任务需要执行,不外乎三种解决方法:
    • 排队:因为一个进程一次只能执行一个任务,只好等前面的任务执行完了,再执行后面的任务。
    • 新建进程:使用fork命令为每个任务新建一个进程。
    • 新建线程:因为进程太耗费资源,所以如今的程序往往允许一个进程包含多个线程,由线程去完成任务。

二、进程创建

  1. 通过fork()系统调用我们可以创建一个和当前进程一样的新进程,通常将新进程称为子进程,而当前进程称为父进程。子进程继承了父进程的整个地址空间,其中包括了进程上下文,堆栈地址,内存信息进程控制块(PCB)等。fork会产生一个和父进程完全相同的子进程,但子进程在此后大多会有exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

    • 所谓创建进程,实际上是创建进程映像中的PCB
      • 进程实体也叫进程映像,由程序段、相关数据段和PCB三部分构成
      • PCB,即进程控制块,系统利用其来描述进程的基本情况和运行状态,进而控制和管理进程
        • PCB是进程存在的唯一标志
      • 进程是动态的,进程实体是静态的
    • 创建进程具体过程:
      • 分配ID与PCB:为新进程分配一个唯一的进程标识号,并申请一个空白的PCB(PCB是有限的)。
        • 若PCB申请失败则创建失败
      • 分配资源:为新进程的程序和数据、以及用户栈分配必要的内存空间(在PCB中体现)。
        • 这里如果资源不足(比如内存空间),并不是创建失败,而是处于阻塞态。
      • 初始化PCB:主要初始化(1)标志信息(2)处理机状态信息(3)处理机控制信息,以及(4)设置进程的优先级等。
      • 调度:如果进程就绪队列能够接纳新进程,就将新进程插入到就绪队列,等待被调度运行。
  2. 那么子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?

    • 在fork之后exec系统调用之前,两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间。也就是说两者的虚间不同,但其对应的物理空间是同一个。
    • 当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
    • 如果不是因为exec,内核会给程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。
    • 如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
  3. 在网上看到还有个细节问题就是,fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程exec系统调用,因无意义的复制而造成效率的下降。

三、进程状态

  1. 三态模型

    • 运行(running)态:进程占有处理器正在运行的状态。
      • 进程已获得CPU,其程序正在执行。在单处理机系统中只有一个进程处于运行状态,在多处理机系统中则有多个进程处于运行状态。
    • 就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行的状态。
      • 当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU便可立即执行,进程这时的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
    • 等待(wait)态:又称阻塞态、睡眠态,指进程不具备运行条件,正在等待某个时间完成的状态。
      • 一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。
  2. 五态模型:在三态模型的基础上增加了新建态(new)和终止态(exit)。

    • 新建态:也称初始态,对应于进程被创建时的状态,尚未进入就绪队列。
      • 进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。
      • 创建一个进程需要通过两个步骤:
        • 为新进程分配所需要资源和建立必要的管理信息
        • 设置该进程为就绪态,并等待被调度执行
    • 终止态:指进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。
      • 处于终止态的进程不再被调度执行,下一步将被系统撤销,最终从系统中消失。
      • 终止一个进程需要两个步骤:
        • 等待操作系统或相关的进程进行善后处理(如抽取信息)
        • 回收占用的资源并被系统删除
      • 进程终止具体过程:
        • 根据被终止进程的ID,检索PCB,从中读出该进程的状态
        • 若被终止进程处于执行状态,立即终止该进程的执行,将处理机资源分配给其他进程
        • 若该进程还有子进程,则应将其所有的子进程终止
        • 将该进程所拥有的资源,或归还给其父进程或归还给操作系统
        • 将该PCB从所在队列(链表)中删除
      • 引起进程终止的事件有:
        • 正常结束
        • 异常结束:如存储区越界、非法指令、I/O故障等
        • 外界干预:如操作员或操作系统干预、父进程请求、父进程终止
  3. 七态模型:在五态模型的基础上增加了挂起就绪态(ready suspend)和挂起等待态(blocked suspend)。三态模型和五态模型都是假设所有进程都在内存中,事实上由于不断的创建进程,当系统资源尤其是内存资源已经不能满足进程运行的要求时必须把某些进程挂起(suspend),对换到磁盘对换区中,释放它占有的某些资源,暂时不参与低级调度,从而起到平滑系统操作负荷的目的。

    • 挂起就绪态:进程具备运行条件,但目前在外存中,只有它被对换到内存才能被调度执行。
    • 挂起等待态:表明进程正在等待某一个事件发生且在外存中。
  4. 进程挂起:挂起是指在操作系统进程管理将前台的进程暂停并转入后台的动作。将进程挂起可以让用户在前台执行其他的进程。挂起的进程通常释放除CPU以外已经占有的系统资源,如内存等。在需要时用户可以恢复进程的运行,将被挂起的进程从后台转入前台,并从暂停处开始继续运行。

    • 挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程。机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存;当条件允许的时候会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作。
    • 引起挂起的原因
      • 终端用户的请求:当终端用户在自己的程序运行期间发现有可疑问题时,希望暂停使自己的程序静止下来。亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态成为“挂起状态”。
      • 父进程的请求:有时父进程希望挂起自己的某个子进程,以便考察和修改子进程,或者协调各子进程间的活动。
      • 负荷调节的需要:当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
      • 操作系统的需要:操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。
      • 对换的需要:为了缓和内存紧张的情况,将内存中处于阻塞状态的进程换至外存上。

四、进程切换

  1. 操作系统为了控制进程的执行,必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,这种行为被称为进程切换,任务切换或上下文切换。
    • 进行进程切换就是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。
      • 从某个进程收回处理器,实质上就是把进程存放在处理器的寄存器中的中间数据找个地方存起来,从而把处理器的寄存器腾出来让其他进程使用。
    • 进程切换实质上就是被中断运行进程与待运行进程的上下文切换
      • 切换新的页表,然后使用新的虚拟地址空间
      • 切换内核栈,加入新的内容(PCB控制块,资源相关),硬件上下文切换
  2. 切换过程
    • 保存处理机上下文,包括程序计数器和其他寄存器
    • 更新PCB信息
    • 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列
    • 选择另一个进程执行,并更新其PCB
    • 更新内存管理的数据结构
    • 恢复处理机上下文

五、进程调度

  1. 无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数,这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。
  2. 进程的调度分为抢占式和非抢占式,也称剥夺式和非剥夺式。
    • 剥夺方式:当一个进程正在运行时,系统可以基于某种原则剥夺已分配给它的处理机,将之分配给其它进程。
    • 非剥夺方式:分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生某事件而阻塞时,才把处理机分配给另一个进程。
  3. 常见调度算法
    • 先进先出算法
    • 短进程优先
    • 轮转法
    • 多级反馈队列
  4. 调度和切换
    • 调度是指决定资源分配给哪个进程的行为,是一种决策行为。
    • 切换是指实际分配的行为,是执行行为。

六、进程间通信

  1. 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信IPC(InterProcess Communication)。

    • 简而言之,进程间通信是指在不同进程之间传播或交换信息,它的本质是进程之间可以看到一份公共资源,而提供这份资源的形式或者提供者不同,造成了通信方式不同。
  2. 通信目的

    • 数据传输:一个进程需要将其数据发送给另一进程,发送的数据量在一个字节到几M字节之间。
    • 共享数据:多个进程操作共享数据
    • 事件通知:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
    • 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
    • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
  3. 分类

    • 无名管道

      • 无名管道,是一种最基本的进程间通信机制。

      • 特点

        • 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
        • 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
        • 它可以看成是一种特殊的文件,不属于其他任何文件系统且只存在于内存中,对于它的读写也可以使用普通的read、write等函数
          • 流管道s_pipe: 去除了第一种限制,可以双向传输(全双工).
      • 过程

        • 父进程创建管道,得到两个⽂件描述符指向管道的两端
        • 父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道
        • 因管道是半双工的,父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端,⽗进程向管道⾥写,⼦进程从管道⾥读,这样就实现了数据由父进程到子进程间的通信,反之则可以使数据流从子进程流向父进程
        • 管道是⽤环形队列实现的
      • 父进程到子进程的管道

      • 库函数

        1
        2
        #include <unistd.h>
        int pipe(int fd[2]);// 返回值:若成功返回0,失败返回-1
    • 有名管道

      • 有名管道,也称FIFO,它是一种文件类型,其通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读取时,FIFO管道中同时清除数据,并且遵循“先进先出”原则。

      • 特点

        • 除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信
        • FIFO可以在无关的进程之间交换数据,与无名管道不同
        • FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
      • 库函数

        • 其中的mode参数与open函数中的mode相同。
        • 一旦创建了一个FIFO,就可以用一般的文件I/O函数操作它。
        • 当open一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
          • 若没有指定O_NONBLOCK(默认),只读open要阻塞到某个其他进程为写而打开此FIFO。类似的,只写open要阻塞到某个其他进程为读而打开它。
          • 若指定了O_NONBLOCK,则只读open立即返回。而只写open将出错返回 -1 如果没有进程已经为读而打开该FIFO,其errno置ENXIO。
        1
        2
        #include <sys/stat.h>
        int mkfifo(const char *pathname, mode_t mode);// 返回值:成功返回0,出错返回-1
    • 消息队列

      • 消息队列,是消息的链接表,存放在内核中,一个消息队列由一个标识符(即队列ID)来标识。

      • 特点

        • 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
        • 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除
        • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
      • 库函数

        • 在以下两种情况下,msgget将创建一个新的消息队列:
          • 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
          • key参数为IPC_PRIVATE。
        • 函数msgrcv在读取消息队列时,type参数有下面几种情况:
          • type = 0,返回队列中的第一个消息;
          • type > 0,返回队列中消息类型为type的第一个消息;
          • type < 0,返回队列中消息类型值小于或等于type绝对值的消息,如果有多个,则取类型值最小的消息。
        1
        2
        3
        4
        5
        #include <sys/msg.h>
        int msgget(key_t key, int flag);// 创建或打开消息队列:成功返回队列ID,失败返回-1
        int msgsnd(int msqid, const void *ptr, size_t size, int flag);// 添加消息:成功返回0,失败返回-1
        int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);// 读取消息:成功返回消息数据的长度,失败返回-1
        int msgctl(int msqid, int cmd, struct msqid_ds *buf);// 控制消息队列:成功返回0,失败返回-1
    • 信号量

      • 信号量(semaphore)是一个计数器,可以用来控制多个进程对共享资源的访问。
        • 它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此主要作为进程间以及同一进程内不同线程之间的同步手段。
        • 信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
      • 特点
        • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
        • 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作
        • 每次对信号量的PV操作不仅限于对信号量值加1或减1,而且可以加减任意正整数
        • 支持信号量组
      • 库函数
        • 当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1;如果是引用一个现有的集合,则将num_sems指定为0。
        • sem_op是一次操作中的信号量的改变量:
          • 若sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。
          • 若sem_op < 0,请求 sem_op 的绝对值的资源。
            • 如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
            • 当相应的资源数不能满足请求时,这个操作与sem_flg有关。
              • sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN。
              • sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
                • 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
                • 此信号量被删除,函数smeop出错返回EIDRM;
                • 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
          • 若sem_op == 0,进程阻塞直到信号量的相应值为0:
            • 当信号量已经为0,函数立即返回。
            • 如果信号量的值不为0,则依据sem_flg决定函数动作:
              • sem_flg指定IPC_NOWAIT,则出错返回EAGAIN。
              • sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
                • 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
                • 此信号量被删除,函数smeop出错返回EIDRM;
                • 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR
          • 在semctl函数中的命令有多种,这里就说两个常用的:
            • SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
            • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
1
2
3
4
5
6
7
8
9
10
11
#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);// 创建或获取一个信号量组:若成功返回信号量集ID
int semop(int semid, struct sembuf semoparray[], size_t numops);// 对信号量组进行操作,改变信号量的值
int semctl(int semid, int sem_num, int cmd, ...);// 控制信号量的相关信息

struct sembuf
{
short sem_num; // 信号量组中对应的序号,0~sem_nums-1
short sem_op; // 信号量值在一次操作中的改变量
short sem_flg; // IPC_NOWAIT, SEM_UNDO
}
  • 共享内存

    • 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

      • 它是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
    • 特点

      • 共享内存是最快的一种IPC,因为进程是直接对内存进行存取,它是针对其他进程间通信方式运行效率低而专门设计的
      • 因为多个进程可以同时操作,所以需要进行同步
      • 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
    • 库函数

      • 当用shmget函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,则将 size 指定为0 。

      • 当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。

      • shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。

      • shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。

        1
        2
        3
        4
        5
        #include <sys/shm.h>
        int shmget(key_t key, size_t size, int flag);// 创建或获取一个共享内存
        void *shmat(int shm_id, const void *addr, int flag);// 连接共享内存到当前进程的地址空间
        int shmdt(void *addr); // 断开与共享内存的连接
        int shmctl(int shm_id, int cmd, struct shmid_ds *buf);// 控制共享内存的相关信息
  • Socket

    • 套解字Socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
  • Streams

    • 其中Socket和Streams支持不同主机上的两个进程IPC
      1. 进程可以是在同一台计算机上,也可能是在网络联通的不同计算机上,根据进程所处位置不同,进程间通信的方法包括两类:
  • 本地过程调用(LPC,Local Procedure Call):LPC用在多任务操作系统中,使得同时运行的任务能互相会话,这些任务共享内存空间使任务同步和互相发送信息。

  • 远程过程调用(RPC,Remote Procedure Call):RPC是一种进程间通信方式,它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的,本质上编写的调用代码基本相同。RPC开始是出现在Sun微系统公司和HP公司的运行UNIX操作系统的计算机中。

    1. 实现
  • 管道

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #include<stdio.h>
    #include<unistd.h>

    int main()
    {
    int fd[2]; // 两个文件描述符
    pid_t pid;
    char buff[20];

    if(pipe(fd) < 0) // 创建管道
    printf("Create Pipe Error!\n");

    if((pid = fork()) < 0) // 创建子进程
    printf("Fork Error!\n");
    else if(pid > 0) // 父进程
    {
    close(fd[0]); // 关闭读端
    write(fd[1], "hello world\n", 12);
    }
    else
    {
    close(fd[1]); // 关闭写端
    read(fd[0], buff, 20);
    printf("%s", buff);
    }

    return 0;
    }
  • FIFO

    • write_fifo.c

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      #include<stdio.h>
      #include<stdlib.h> // exit
      #include<fcntl.h> // O_WRONLY
      #include<sys/stat.h>
      #include<time.h> // time
      #include <unistd.h> // for close

      int main()
      {
      int fd;
      int n, i;
      char buf[1024];
      time_t tp;

      printf("I am %d process.\n", getpid()); // 说明进程ID

      if((fd = open("fifo1", O_WRONLY)) < 0) // 以写打开一个FIFO
      {
      perror("Open FIFO Failed");
      exit(1);
      }

      for(i=0; i<10; ++i)
      {
      time(&tp); // 取系统当前时间
      n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));
      printf("Send message: %s", buf); // 打印
      if(write(fd, buf, n+1) < 0) // 写入到FIFO中
      {
      perror("Write FIFO Failed");
      close(fd);
      exit(1);
      }
      sleep(1); // 休眠1秒
      }

      close(fd); // 关闭FIFO文件
      return 0;
      }
    • read_fifo.c

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      #include<stdio.h>
      #include<stdlib.h>
      #include<errno.h>
      #include<fcntl.h>
      #include<sys/stat.h>
      #include <unistd.h> // for close

      int main()
      {
      int fd;
      int len;
      char buf[1024];

      if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 创建FIFO管道
      perror("Create FIFO Failed");

      if((fd = open("fifo1", O_RDONLY)) < 0) // 以读打开FIFO
      {
      perror("Open FIFO Failed");
      exit(1);
      }

      while((len = read(fd, buf, 1024)) > 0) // 读取FIFO管道
      printf("Read message: %s", buf);

      close(fd); // 关闭FIFO文件
      return 0;
      }
    • 分别运行两个c文件 gcc write_fifo.c -o write.out/gcc read_fifo.c -o read.out

    • 分别执行两个可执行文件 write.out/read.out

  • 消息队列

    • msg-server.c

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      #include <stdio.h>
      #include <stdlib.h>
      #include <sys/msg.h>
      #include <unistd.h> // for close

      // 用于创建一个唯一的key
      #define MSG_FILE "/etc/passwd"

      // 消息结构
      struct msg_form {
      long mtype;
      char mtext[256];
      };

      int main()
      {
      int msqid;
      key_t key;
      struct msg_form msg;

      // 获取key值
      if((key = ftok(MSG_FILE,'z')) < 0) {
      perror("ftok error");
      exit(1);
      }

      // 打印key值
      printf("Message Queue - Server key is: %d.\n", key);

      // 创建消息队列
      if ((msqid = msgget(key, IPC_CREAT|0777)) == -1) {
      perror("msgget error");
      exit(1);
      }

      // 打印消息队列ID及进程ID
      printf("My msqid is: %d.\n", msqid);
      printf("My pid is: %d.\n", getpid());

      // 循环读取消息
      for(;;)
      {
      msgrcv(msqid, &msg, 256, 888, 0);// 返回类型为888的第一个消息
      printf("Server: receive msg.mtext is: %s.\n", msg.mtext);
      printf("Server: receive msg.mtype is: %d.\n", msg.mtype);

      msg.mtype = 999; // 客户端接收的消息类型
      sprintf(msg.mtext, "hello, I'm server %d", getpid());
      msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
      }
      return 0;
      }
    • msg-client.c

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      #include <stdio.h>
      #include <stdlib.h>
      #include <sys/msg.h>

      // 用于创建一个唯一的key
      #define MSG_FILE "/etc/passwd"

      // 消息结构
      struct msg_form {
      long mtype;
      char mtext[256];
      };

      int main()
      {
      int msqid;
      key_t key;
      struct msg_form msg;

      // 获取key值
      if ((key = ftok(MSG_FILE, 'z')) < 0) {
      perror("ftok error");
      exit(1);
      }

      // 打印key值
      printf("Message Queue - Client key is: %d.\n", key);

      // 打开消息队列
      if ((msqid = msgget(key, IPC_CREAT|0777)) == -1) {
      perror("msgget error");
      exit(1);
      }

      // 打印消息队列ID及进程ID
      printf("My msqid is: %d.\n", msqid);
      printf("My pid is: %d.\n", getpid());

      // 添加消息,类型为888
      msg.mtype = 888;
      sprintf(msg.mtext, "hello, I'm client %d", getpid());
      msgsnd(msqid, &msg, sizeof(msg.mtext), 0);

      // 读取类型为777的消息
      msgrcv(msqid, &msg, 256, 999, 0);
      printf("Client: receive msg.mtext is: %s.\n", msg.mtext);
      printf("Client: receive msg.mtype is: %d.\n", msg.mtype);
      return 0;
      }
  • 信号量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/sem.h>

    // 联合体,用于semctl初始化
    union semun {
    int val; /*for SETVAL*/
    struct semid_ds *buf;
    unsigned short *array;
    };

    // 初始化信号量
    int init_sem(int sem_id, int value) {
    union semun tmp;
    tmp.val = value;
    if(semctl(sem_id, 0, SETVAL, tmp) == -1) {
    perror("Init Semaphore Error");
    return -1;
    }
    return 0;
    }

    // 若信号量值为1,获取资源并将信号量值-1
    // 若信号量值为0,进程挂起等待
    int sem_p(int sem_id) {
    struct sembuf sbuf;
    sbuf.sem_num = 0; /*序号*/
    sbuf.sem_op = -1; /*P操作*/
    sbuf.sem_flg = SEM_UNDO;

    if(semop(sem_id, &sbuf, 1) == -1)
    {
    perror("P operation Error");
    return -1;
    }
    return 0;
    }

    // 释放资源并将信号量值+1
    // 如果有进程正在挂起等待,则唤醒它们
    int sem_v(int sem_id) {
    struct sembuf sbuf;
    sbuf.sem_num = 0; /*序号*/
    sbuf.sem_op = 1; /*V操作*/
    sbuf.sem_flg = SEM_UNDO;

    if(semop(sem_id, &sbuf, 1) == -1) {
    perror("V operation Error");
    return -1;
    }
    return 0;
    }

    // 删除信号量集
    int del_sem(int sem_id) {
    union semun tmp;
    if(semctl(sem_id, 0, IPC_RMID, tmp) == -1) {
    perror("Delete Semaphore Error");
    return -1;
    }
    return 0;
    }


    int main() {
    int sem_id; // 信号量集ID
    key_t key;
    pid_t pid;

    // 获取key值
    if((key = ftok(".", 'z')) < 0) {
    perror("ftok error");
    exit(1);
    }

    // 创建信号量集,其中只有一个信号量
    if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1) {
    perror("semget error");
    exit(1);
    }

    // 初始化:初值设为0资源被占用
    init_sem(sem_id, 0);

    if((pid = fork()) == -1) {
    perror("Fork Error");
    } else if(pid == 0) {
    sleep(2);
    printf("Process child: pid=%d\n", getpid());
    sem_v(sem_id);
    } else {
    sem_p(sem_id);
    printf("Process father: pid=%d\n", getpid());
    sem_v(sem_id);
    del_sem(sem_id);
    }
    return 0;
    }

    //如果不加信号量,则父进程会先执行完毕。这里加了信号量让父进程等待子进程执行完以后再执行。
  • 共享内存

    • 实现思路:

      • 共享内存用来传递数据
      • 信号量用来同步
      • 消息队列用来在客户端修改了共享内存后通知服务器读取
    • server.c

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      #include<stdio.h>
      #include<stdlib.h>
      #include<sys/shm.h>
      #include<sys/sem.h>
      #include<sys/msg.h>
      #include<string.h>

      // 消息队列结构
      struct msg_form {
      long mtype;
      char mtext;
      };

      // 联合体,用于semctl初始化
      union semun {
      int val; /*for SETVAL*/
      struct semid_ds *buf;
      unsigned short *array;
      };

      // 初始化信号量
      int init_sem(int sem_id, int value) {
      union semun tmp;
      tmp.val = value;
      if(semctl(sem_id, 0, SETVAL, tmp) == -1)
      {
      perror("Init Semaphore Error");
      return -1;
      }
      return 0;
      }

      // P操作:
      // 若信号量值为1,获取资源并将信号量值-1
      // 若信号量值为0,进程挂起等待
      int sem_p(int sem_id) {
      struct sembuf sbuf;
      sbuf.sem_num = 0; /*序号*/
      sbuf.sem_op = -1; /*P操作*/
      sbuf.sem_flg = SEM_UNDO;

      if(semop(sem_id, &sbuf, 1) == -1) {
      perror("P operation Error");
      return -1;
      }
      return 0;
      }

      // V操作:
      // 释放资源并将信号量值+1
      // 如果有进程正在挂起等待,则唤醒它们
      int sem_v(int sem_id) {
      struct sembuf sbuf;
      sbuf.sem_num = 0; /*序号*/
      sbuf.sem_op = 1; /*V操作*/
      sbuf.sem_flg = SEM_UNDO;

      if(semop(sem_id, &sbuf, 1) == -1) {
      perror("V operation Error");
      return -1;
      }
      return 0;
      }

      // 删除信号量集
      int del_sem(int sem_id) {
      union semun tmp;
      if(semctl(sem_id, 0, IPC_RMID, tmp) == -1) {
      perror("Delete Semaphore Error");
      return -1;
      }
      return 0;
      }

      // 创建一个信号量集
      int creat_sem(key_t key) {
      int sem_id;
      if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1) {
      perror("semget error");
      exit(-1);
      }
      init_sem(sem_id, 1); /*初值设为1资源未占用*/
      return sem_id;
      }


      int main() {
      key_t key;
      int shmid, semid, msqid;
      char *shm;
      char data[] = "this is server";
      struct shmid_ds buf1; /*用于删除共享内存*/
      struct msqid_ds buf2; /*用于删除消息队列*/
      struct msg_form msg; /*消息队列用于通知对方更新了共享内存*/

      // 获取key值
      if((key = ftok(".", 'z')) < 0) {
      perror("ftok error");
      exit(1);
      }

      // 创建共享内存
      if((shmid = shmget(key, 1024, IPC_CREAT|0666)) == -1) {
      perror("Create Shared Memory Error");
      exit(1);
      }

      // 连接共享内存
      shm = (char*)shmat(shmid, 0, 0);
      if((int)shm == -1) {
      perror("Attach Shared Memory Error");
      exit(1);
      }


      // 创建消息队列
      if ((msqid = msgget(key, IPC_CREAT|0777)) == -1) {
      perror("msgget error");
      exit(1);
      }

      // 创建信号量
      semid = creat_sem(key);

      // 读数据
      while(1) {
      msgrcv(msqid, &msg, 1, 888, 0); /*读取类型为888的消息*/
      if(msg.mtext == 'q') /*quit - 跳出循环*/
      break;
      if(msg.mtext == 'r') /*read - 读共享内存*/
      {
      sem_p(semid);
      printf("%s\n",shm);
      sem_v(semid);
      }
      }

      // 断开连接
      shmdt(shm);

      /*删除共享内存、消息队列、信号量*/
      shmctl(shmid, IPC_RMID, &buf1);
      msgctl(msqid, IPC_RMID, &buf2);
      del_sem(semid);
      return 0;
      }
    • client.c

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      #include<stdio.h>
      #include<stdlib.h>
      #include<sys/shm.h> // shared memory
      #include<sys/sem.h> // semaphore
      #include<sys/msg.h> // message queue
      #include<string.h> // memcpy

      // 消息队列结构
      struct msg_form {
      long mtype;
      char mtext;
      };

      // 联合体,用于semctl初始化
      union semun
      {
      int val; /*for SETVAL*/
      struct semid_ds *buf;
      unsigned short *array;
      };

      // P操作:
      // 若信号量值为1,获取资源并将信号量值-1
      // 若信号量值为0,进程挂起等待
      int sem_p(int sem_id)
      {
      struct sembuf sbuf;
      sbuf.sem_num = 0; /*序号*/
      sbuf.sem_op = -1; /*P操作*/
      sbuf.sem_flg = SEM_UNDO;

      if(semop(sem_id, &sbuf, 1) == -1)
      {
      perror("P operation Error");
      return -1;
      }
      return 0;
      }

      // V操作:
      // 释放资源并将信号量值+1
      // 如果有进程正在挂起等待,则唤醒它们
      int sem_v(int sem_id)
      {
      struct sembuf sbuf;
      sbuf.sem_num = 0; /*序号*/
      sbuf.sem_op = 1; /*V操作*/
      sbuf.sem_flg = SEM_UNDO;

      if(semop(sem_id, &sbuf, 1) == -1)
      {
      perror("V operation Error");
      return -1;
      }
      return 0;
      }


      int main()
      {
      key_t key;
      int shmid, semid, msqid;
      char *shm;
      struct msg_form msg;
      int flag = 1; /*while循环条件*/

      // 获取key值
      if((key = ftok(".", 'z')) < 0)
      {
      perror("ftok error");
      exit(1);
      }

      // 获取共享内存
      if((shmid = shmget(key, 1024, 0)) == -1)
      {
      perror("shmget error");
      exit(1);
      }

      // 连接共享内存
      shm = (char*)shmat(shmid, 0, 0);
      if((int)shm == -1)
      {
      perror("Attach Shared Memory Error");
      exit(1);
      }

      // 创建消息队列
      if ((msqid = msgget(key, 0)) == -1)
      {
      perror("msgget error");
      exit(1);
      }

      // 获取信号量
      if((semid = semget(key, 0, 0)) == -1)
      {
      perror("semget error");
      exit(1);
      }

      // 写数据
      printf("***************************************\n");
      printf("* IPC *\n");
      printf("* Input r to send data to server. *\n");
      printf("* Input q to quit. *\n");
      printf("***************************************\n");

      while(flag)
      {
      char c;
      printf("Please input command: ");
      scanf("%c", &c);
      switch(c)
      {
      case 'r':
      printf("Data to send: ");
      sem_p(semid); /*访问资源*/
      scanf("%s", shm);
      sem_v(semid); /*释放资源*/
      /*清空标准输入缓冲区*/
      while((c=getchar())!='\n' && c!=EOF);
      msg.mtype = 888;
      msg.mtext = 'r'; /*发送消息通知服务器读数据*/
      msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
      break;
      case 'q':
      msg.mtype = 888;
      msg.mtext = 'q';
      msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
      flag = 0;
      break;
      default:
      printf("Wrong input!\n");
      /*清空标准输入缓冲区*/
      while((c=getchar())!='\n' && c!=EOF);
      }
      }

      // 断开连接
      shmdt(shm);

      return 0;
      }

      当scanf()输入字符或字符串时,缓冲区中遗留下了\n,所以每次输入操作后都需要清空标准输入的缓冲区。但是由于gcc编译器不支持fflush(stdin)(它只是标准C的扩展),所以我们使用了替代方案:`while((c=getchar())!='\n' && c!=EOF);`

三、参考

  1. 参考一
  2. 参考二
  3. 参考三
  4. 参考四
  5. 参考五

线程

一、线程

  1. 定义:操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

    • 由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。
  2. 组成

    • 由线程ID、程序计数器、寄存器集合和堆栈组成
    • 线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,与同属一个进程的其他线程共享进程所拥有的全部资源
  3. 优点

    • 易于调度。
    • 提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
    • 开销少。创建线程比创建进程要快,所需开销很少。
    • 利于充分发挥多处理器的功能。
  1. 对比
    • 一个程序至少有一个进程,一个进程至少有一个线程。线程(Thread)是进程的一个实体,是CPU调度和分派的基本单位;
    • 进程拥有独立的内存单元,而多个线程共享内存。从而线程效率更高;
    • 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮;
    • 进程切换时,耗费资源较大,效率要差一些;
    • 进程是系统资源分配的基本单位,线程是调度的基本单位。

二、临界区

  1. 临界区:一种公有的资源或者共享数据,它可以被多个线程使用。
    • 临界区资源一次只能被一个线程使用,其它线程必须等待上一个线程执行完成之后,才能使用。
    • 临界区资源是多线程之间重要保护对象,当临界区资源同时被多个线程访问时,容易出现错误。

三、阻塞与非阻塞

  1. 当一个线程占用了临界区资源,那么其它需要使用这个资源的线程都必须在这个临界区上等待。等待会导致线程挂起,这样就形成了阻塞。
    • 如果占用资源的线程一直没有释放资源,那么其它的线程在这个临界区上都不能继续工作。
  2. 非阻塞表明多个线程之间的执行是不会相互影响的。

四、参考

  1. 参考一
  2. 参考二
  3. 参考三

协程