Lazy loaded image
🥳嵌入式 Linux开发
Lazy loaded image进程和进程间通信
Words 10024Read Time 26 min
2024-11-27
2025-3-21
type
date
slug
category
icon
password

一、介绍

1.1 初学者必需知识

  1. 在 Linux 中输入可执行文件的相对路径或者绝对路径就可以运行程序,比如 ./app arg1 arg2,程序执行过程是,shell 进程解析命令行参数并将参数传递给加载器,加载器加载应用程序,将参数传递给引导代码(编译链接时引导代码和 main 函数一起链接到应用程序中),引导程序再调用 main() 函数,最终参数传递给 main() 函数。
  1. 程序的结束其实就是进程结束,分为正常终止和异常终止。终止可以注册终止处理函数
    1. 进程是一个可执行程序的实例,是一个动态过程,有启动、运行和终止生命周期。进程号(pid_t)是标志系统中某一个进程标志 。

    1.2 Linux 进程是什么?

    进程其实就是一个可执行程序的实例,当应用程序被加载到内存中运行之后它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。
    Linux 系统下的每一个进程都有一个进程号(process ID,简称 PID),进程号是一个正数,用于唯一标识系统中的某一个进程。
    • getpid() 获取本进程进程号
      • getppid() 获取父进程进程号
         

        1.2.1 进程环境变量

        1. 每个进程都有一组与其相关的环境变量,以字符串形式存储在列表中。进程的环境变量是从父进程中继承过来的,譬如在 shell 终端下执行一个应用程序,那么该进程的环境变量就是从其父进程(shell 进程)中继承过来的。新的进程在创建之前,会继承其父进程的环境变量副本。
          命令
          说明
          env
          查看进程所有环境变量
          export LINUX_APP=123456
          添加环境变量
          export -n LINUX_APP
          删除环境变量
          应用程序中操作环境变量
          命令
          说明
          extern char **environ;
          environ 是一个全局变量,存储环境变量
          char *getenv(const char *name);
          获取某个指定的环境变量
          int putenv(char *string);
          添加/修改环境变量
          int setenv(const char *name, const char *value, int overwrite);
          添加/修改环境变量
          推荐,但会产生内存泄漏
          NAME=value ./app
          添加环境变量更简单方法
          int unsetenv(const char *name);
          移除参数 name 标识的环境变量
          int clearenv(void);
          清空环境变量
          environ = NULL;
          清空环境变量

          1.2.2 进程的内存布局

          • Stack (栈):存放局部变量和函数调用信息,从高地址向低地址增长
          • Heap (堆):动态内存分配区域,从低地址向高地址增长
          • BSS段:存放未初始化的全局/静态变量
          • Data段:存放已初始化的全局/静态变量
          • Text/Code段:存放程序执行指令

          1.3 Linux 进程用在哪?

          在诸多的应用中,创建多个进程是任务分解时行之有效的方法,譬如:
          • 某一网络服务器进程可在监听客户端请求的同时,为处理每一个请求事件而创建一个新的子进程,与此同时,服务器进程会继续监听更多的客户端连接请求。
          • 在一个大型的应用程序任务中,创建子进程通常会简化应用程序的设计,同时提高了系统的并发性(即同时能够处理更多的任务或请求,多个进程在宏观上实现同时运行)。

          二、Linux 进程怎么用?

          2.1 fork() 函数

          调用 fork()函数创建一个新的进程
          参数
          描述
          返回值
          成功:1. 父进程中返回子进程的PID 2. 子进程中返回值为0 失败: 父进程返回值-1,不创建子进程,并设置 errno
          注意1:调用后,存在两个进程,每个进程都会从fork()函数的返回处继续执行,会导致调用 fork()返回两次值,子进程返回一个值、父进程返回一个值。在程序代码中,可通过返回值来区分是子进程还是父进程 注意2:fork()调用成功后,子进程和父进程会继续执行 fork()调用之后的指令,子进程、父进程各自在自己的进程空间中运行。
          • 共享代码段
          • 子进程拷贝了父进程的数据段、堆、栈以及继承了父进程打开的文件描述符,但不共享。
          示例:
          • 在调用了 fork()之后,父、子进程中一般只有一个会通过调用 exit()退出进程,而另一个则应使用_exit()退出。具体原因参考下文2.6 进程的生命周期
          ⚠️
          子进程创建开始,父子进程是共享数据段、堆、栈内存页的。只有在父子进程修改共享内存页,才会产生拷贝。如下过程:
          1. 创建子进程:父进程调用 fork(),操作系统为子进程创建页表,指向父进程的内存页,并将这些页标记为只读。
          1. 尝试写入:如果父进程或子进程尝试写入共享的内存页,会触发页错误(page fault)。
          1. 复制页:操作系统捕获页错误,复制该内存页,子进程的页表更新为指向新复制的页,在新页上执行写操作,父进程的页保持不变。

          2.2 文件共享

          2.2.1 文件描述符继承,父子进程偏移量共享

          输出结果
          AABBAABBAABBAABB1122112211221122

          2.2.2 不同的文件描述符,父子进程偏移量不共享

          输出结果
          1122112211221122AABB
          子进程重新打开,文件偏移量不共享,从头开始写入四次“1122”。

          2.3 fork() 函数使用场景

          fork()函数有以下两种用法:
          • 父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用 fork()创建一个子进程,使子进程去处理此请求、而父进程可以继续等待下一个服务请求。
          • 一个进程要执行不同的程序。譬如在程序 app1 中调用 fork()函数创建了子进程,此时子进程是要去执行另一个程序 app2,也就是子进程需要执行的代码是 app2 程序对应的代码,子进程将从 app2程序的 main 函数开始运行。这种情况,通常在子进程从 fork()函数返回之后立即调用 exec 族函数来实现,关于 exec 函数将在后面内容向大家介绍。

          2.4 fork() 之后的竞争条件

          问题说明
          调用 fork 之后,无法确定父、子两个进程谁将率先访问 CPU,也就是说无法确认谁先被系统调用运行。
          解决方法
          采用信号同步技术,实现指定顺序执行。
          输出结果

          2.5 vfork() 函数

          2.5.1 为什么需要vfork()?

          存在问题
          1. fork() 复制父进程中的数据段和堆栈段中的绝大部分内容,耗时长、效率低、资源浪费
          1. 调用 exec 函数,子进程不再执行父程序中的代码段,而是执行新程序的代码段,再用fork(),存粹浪费时间、降低效率
          功能
          vfork()是为子进程立即执行 exec()新的程序而专门设计的,与fork() 函数主要有两个区别
          1. vfork() 函数并不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec(或_exit),于是也就不会引用该地址空间的数据
          1. vfork() 保证子进程先运行,子进程调用 exec 之后父进程才可能被调度运行

          2.5.2 函数接口

          2.6 进程的生命周期

          1. 进程开始:PID =1, init 进程,内核启动
          1. 进程终止:正常终止和异常终止
            1. exit() 终止执行步骤
              1. 如果程序中注册了进程终止处理函数,那么会调用终止处理函数
              2. 刷新 stdio 流缓冲区。
              3. 执行_exit()系统调用。
            2. 子进程使用_exit()退出、而父进程则使用 exit()退出。其原因就在于调用 exit()函数终止进程时会刷新进程的 stdio 缓冲区
          如上程序,由于子进程调用exit(0)退出程序,会刷新进程的stdio缓冲区,因此会造成Hello World!打印两遍。可以采用以下任一方法来避免重复的输出结果:
          1. 对于行缓冲设备,可以加上对应换行符,譬如 printf 打印输出字符串时在字符串后面添加\n 换行符,对于 puts()函数来说,本身会自动添加换行符;
          1. 在调用 fork()之前,使用函数 fflush()来刷新 stdio 缓冲区,当然,作为另一种选择,也可以使用setvbuf()和 setbuf() 来关闭 stdio 流的缓冲功能;
          1. 子进程调用_exit()退出进程、而非使用 exit(),调用_exit()在退出时便不会刷新 stdio 缓冲区,这也解释前面为什么我们要在子进程中使用_exit()退出这样做的一个原因。将示例中子进程的退出操作 exit()替换成_exit()进行测试,打印的结果便只会显示一次字符串。

          2.5 监视子进程和进程回收

          在很多应用程序的设计中,父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视,本小节我们就来学习下如何通过系统调用 wait()以及其它变体来监视子进程的状态改变。

          2.5.1 wait() 函数

          获取子进程的终止状态信息
          • 若所有子进程都还在运行,阻塞等待子进程终止。
          • 没有子进程, wait()将返回错误,也就是返回-1、并且会将 errno 设置为 ECHILD。
          • 若已经有子进程终止,wait() 不会阻塞,会立即回收子进程的一些资源。
          参数
          描述
          status
          参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。
          返回值
          若成功则返回终止的子进程对应的进程号;失败则返回-1。errno 设置为 ECHILD
          可通过以下宏来检查 status 参数:
          • WIFEXITED(status):如果子进程正常终止,则返回 true;
          • WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit()时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过WEXITSTATUS 宏转换;
          • WIFSIGNALED(status):如果子进程被信号终止,则返回 true;
          • WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
          • WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;
          wait() 缺点
          1. 只能按照顺序处理进程,根据进程终止顺序
          1. 只能阻塞等待
          1. 只能发现被终止进程,对于SIGSTOP信号(暂停运行),SIGCONT(恢复执行)情况就无能为力了。

          2.5.2 waitpid() 函数

           
          参数
          描述
          pid
          参数 pid 用于表示需要等待的某个具体子进程 ⚫ 如果 pid 大于 0,表示等待进程号为 pid 的子进程; ⚫ 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程; ⚫ 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程; ⚫ 如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。
          status
          参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。
          options
          一个位掩码 1. WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待。 2. WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息; 3. WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。
          返回值
          若成功则返回终止的子进程对应的进程号;失败则返回-1。0 时表示没有发生改变
          示例
          输出结果
           

          2.5.3 僵尸进程与孤儿进程

          孤儿进程:父进程先于子进程结束,在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程。
          • 子进程调用 getppid()将返回 1,init 进程变成了孤儿进程的“养父”;这是判定某一子进程的“生父”是否还“在世”的方法之一
           
          僵尸进程:子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。
          • 当父进程调用 wait()(或其变体,下文不再强调)为子进程“收尸”后,僵尸进程就会被内核彻底删除。
          • 另外一种情况,如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(),故而从系统中移除僵尸进程。
          如果父进程创建了某一子进程,子进程已经结束,而父进程还在正常运行,但父进程并未调用wait()回收子进程,此时子进程变成一个僵尸进程。首先来说,这样的程序设计是有问题的,如果系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号SIGKILL也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样init进程将会接管这些僵尸进程,从而将它们从系统中清理掉!所以,在我们的一个程序设计中,一定要监视子进程的状态变化,如果子进程终止了,要调用wait()将其回收,避免僵尸进程。
           
          notion image
          状态栏显示的是“Z”(zombie,僵尸),表示它是一个僵尸进程。
           
          SIGCHLD 信号
          • 子进程状态改变时(终止、暂停或恢复),父进程会收到 SIGCHLD 信号
          • 进程的终止属于异步事件
            • → 捕获 SIGCHLD 信号,绑定信号处理函数,调用 wait()收回子进程
          • 当 SIGCHLD 信号处理函数正在为一个终止的子进程“收尸”时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一次 SIGCHLD 信号,结果是,父进程的SIGCHLD 信号处理函数每次只调用一次wait(),那么就会导致有些僵尸进程成为“漏网之鱼”。
            • → 循环非阻塞方式调用 waitpid()
          示例程序

          2.8 执行新程序

          2.8.1 execve() 函数

           
          参数
          描述
          filename
          参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
          argv
          参数argv则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于main(intargc,char*argv[])函数的第二个参数argv,且格式也与之相同,是由字符串指针所组成的数组,以NULL结束。 argv[0]对应的便是新程序自身路径名。
          envp
          参数envp其实对应于新程序的environ数组 所指向的字符串格式为 name=value
          返回值
          execve 调用成功将不会返回;失败将返回-1,并设置 errno。

          2.8.2 exec 库函数

           
          • execv()的argv 参数与 execve()的 argv 参数相同,也是字符串指针数组;
          • execl()把参数列表依次排列,使用可变参数形式传递,本质上也是多个字符串,以 NULL 结尾
          • execlp()和 execvp()在 execl()和 execv()基础上加了一个 p,这个 p 其实表示的是 PATH;execlp()和execvp()则允许只提供新程序文件名,系统会在由环境变量PATH所指定的目录列表中寻找相应的可执行文件,如果执行的新程序是一个Linux命令,这将很有用
          • execle()和 execvpe()这两个函数在命名上加了一个 e,这个 e 其实表示的是 environment 环境变量,意味着这两个函数可以指定自定义的环境变量列表给新程序

          2.8.3 system()函数

          执行任意 shell 命令
          参数
          描述
          filename
          参数 command 指向需要执行的 shell 命令,以字符串的形式提供,譬如"ls -al"、"echo HelloWorld"等。
          返回值
          ⚫ 当参数 command 为 NULL,如果 shell 可用则返回一个非 0 值,若不可用则返回 0;针对一些非UNIX 系统,该系统上可能是没有 shell 的,这样就会导致 shell 不可能;如果 command 参数不为NULL,则返回值从以下的各种情况所决定。 ⚫ 如果无法创建子进程或无法获取子进程的终止状态,那么 system()返回-1; ⚫ 如果子进程不能执行 shell,则 system()的返回值就好像是子进程通过调用_exit(127)终止了; ⚫ 如果所有的系统调用都成功,system()函数会返执行 command 的 shell 进程的终止状态。

          2.9 进程状态和关系

          2.9.1 进程状态

          Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
          notion image

          2.9.2 进程关系

          介绍完进程状态之后,接下来聊一聊进程关系,在 Linux 系统下,每个进程都有自己唯一的标识:进程号(进程 ID、PID),也有自己的生命周期,进程都有自己的父进程、而父进程也有父进程,这就形成了一个以 init 进程为根的进程家族树;当子进程终止时,父进程会得到通知并能取得子进程的退出状态。
          除此之外,进程间还存在着其它一些层次关系,譬如进程组和会话;所以,由此可知,进程间存在着多种不同的关系,主要包括:无关系(相互独立)、父子进程关系、进程组以及会话。
          1. 无关系
          1. 父子进程关系 fork() 创建
          1. 进程组:是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或者在功能上有联系。方便对进程进行管理。
            1. 获取进程组ID
              程序输出:
              进程组 ID<23>---getpgrp() 进程组 ID<23>---getpgid(0) 进程组 ID<23>---getpgid(23)
              设置进程组ID
              程序输出:
              更改前进程组 ID<32> 更改后进程组 ID<32>
          1. 会话:会话是一个或多个进程组的集合
            1. 每个会话都有一个会话首领(leader),即创建会话的进程。会话的首领进程的进程组 ID 将作为该会话的标识,也就是会话 ID(sid)。
              当用户在某个终端登录时,一个新的会话就开始了;当我们在 Linux 系统下打开了多个终端窗口时,实际上就是创建了多个终端会话。

          2.10 守护进程

          2.10.1 何为守护进程?

          守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程。其特点如下:
          1. 不受用户登录注销的影响,一直运行着、直到系统关机。
          1. 脱离终端在后台运行。
          守护进程是一种很有用的进程。Linux 中大多数服务器就是用守护进程实现的,譬如,Internet 服务器inetd、Web 服务器 httpd 等。同时,守护进程完成许多系统任务,譬如作业规划进程 crond 等。

          2.10.2 编写守护进程程序

          1. 创建子进程、终止父进程
            1. 子进程拥有自己PID,保证了子进程不是一个进程组的组长进程
          1. 子进程调用 setsid 创建会话
            1. 调用 setsid()会使得子进程创建一个新的会话,子进程成为新会话的首领进程,同样也创建了新的进程组、子进程成为组长进程,此时创建的会话将没有控制终端。
            2. 让子进程摆脱原会话的控制、让子进程摆脱原进程组的控制和让子进程摆脱原控制终端的控制。
            3. 使子进程完全独立出来,从而脱离所有其他进程的控制。
          1. 将工作目录更改为根目录
            1. 子进程是继承了父进程的当前工作目录
            2. 将子进程工作目录更改为根目录,方便后期使用。
          1. 重设文件权限掩码 umask
            1. fork 函数新建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦
            2. 把文件权限掩码设置为 0,确保子进程有最大操作权限、这样可以大大增强该守护进程的灵活性。
          1. 关闭不再需要的文件描述符
            1. 子进程继承了父进程的所有文件描述符,这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载
          1. 将文件描述符号为 0、1、2 定位到/dev/null
            1. 将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null
            2. 这使得守护进程的输出无处显示、也无处从交互式用户那里接收输入。
          1. 其它:忽略 SIGCHLD 信号
            1. 并发服务器进程,如果父进程 wait 等待子进程退出,将又会增加父进程的负担、也就是增加服务器的负担,影响服务器进程的并发性能。
            2. 可以将 SIGCHLD 信号的处理方式设置为SIG_IGN,也就是忽略该信号,可让内核将僵尸进程转交给 init 进程去处理,这样既不会产生僵尸进程、又省去了服务器进程回收子进程所占用的时间。

          2.11 单例运行

          单例运行指程序只能被执行一次,只要该程序没有结束,就无法再次运行。对于某些场景下需要实现单例运行模式,譬如系统中守护进程,这些守护进程一般都是服务器进程,服务器程序只需要运行一次即可,能够在系统整个的运行过程中提供相应的服务支持,多次同时运行并没有意义、甚至还会带来错误!

          2.11.1 通过文件存在与否进行判断

          用一个文件的存在与否来做标志,在程序运行正式代码之前,先判断一个特定的文件是否存在。
          • 如果存在则表明进程已经运行,此时应该立马退出;
          • 如果不存在则表明进程没有运行,然后创建该文件,当程序结束时再删除该文件即可!
          • 以 O_RDONLY | O_CREAT | O_EXCL 的方式打开文件,如果文件不存在则创建文件,如果文件存在则 open 会报错返回-1。
          • 使用 atexit 注册进程终止处理函数,当程序退出时,使用 remove()删除该文件。
          这种方法虽然简答,但应用中可能会出现以下问题:
          1. 若使用_exit() 退出。不执行回调,则无法执行 delete_file() 函数。
          1. 若程序异常退出,无法执行进程终止处理函数。
          1. 掉电关机,重启后文件仍存在,导致程序无法执行。
          针对第一种情况可以通过使用exit()函数退出,第三种,可以将文件放置到系统/tmp 目录下,/tmp 是一个临时文件系统,当系统重启之后/tmp 目录下的文件就会被销毁。针对第二种,有一种解决办法便是设置信号处理方式为忽略信号,或者针对某些信号注册信号处理函数,譬如 SIGTERM、SIGINT 等,在信号处理函数中删除文件然后再退出进程。但依然有个问题,并不是所有信号都可被忽略或捕获的,譬如SIGKILL 和 SIGSTOP,这两个信号是无法被忽略和捕获的

          2.11.2 使用文件锁

          第一种方法不太可靠,下面介绍使用文件锁方式来实现单例运行。
          文件锁方式同样要通过一个特定的文件来实现,当程序启动之后,首先打开该文件,调用 open 时一般使用O_WRONLY | O_CREAT 标志,当文件不存在则创建该文件。
          然后尝试去获取文件锁
          • 若是成功获取,则将程序的进程号(PID)写入到该文件中,写入后不要关闭文件或解锁(释放文件锁),保证进程一直持有该文件锁;
          • 若是程序获取锁失败,代表程序已经被运行、则退出本次启动。
          • 打开文件之后使用 flock 尝试获取文件锁,调用 flock()时指定了互斥锁标志 LOCK_NB,意味着同时只能有一个进程拥有该锁。
          • 这种机制在一些程序尤其是服务器程序中很常见,服务器程序使用这种方法来保证程序的单例模式运行;在 Linux 系统中/var/run/目录下有很多以.pid 为后缀结尾的文件,这个实际上是为了保证程序以单例模式运行而设计的,作为程序实现单例模式运行所需的特定文件。
          • 如果我们要去实现一个以单例模式运行的程序,譬如一个守护进程,那么也应该将这个特定文件放置于 Linux 系统/var/run/目录下,并且文件的命名方式为 name.pid(name 表示进程名)。

          三、进程间通信简介

          3.1 为什么需要进程间通信

          1. 同一个进程的不同模块(譬如不同的函数)之间进行通信都是很简单的,譬如使用全局变量等
          1. 大多数中小型应用程序不会设计成多进程程序,使用单进程+多线程程序
          1. 复杂、大型的应用程序会考虑设计成多进程程序,比如GUI、服务区应用程序等

          3.2 进程通信来龙去脉

          notion image
           
          AT&T 贝尔实验室,对 UNIX 早期的进程间通信手段进行了系统的改进和扩充,形成了“System V IPC”,通信进程局限在单个计算机内。
          BSD(加州大学伯克利分校的伯克利软件发布中心),形成了基于套接字(Socket,也就是网络)的进程间通信机制。
          • UNIX IPC:管道、FIFO、信号;
          • System V IPC:信号量、消息队列、共享内存;
          • POSIX IPC:信号量、消息队列、共享内存;
          • Socket IPC:基于 Socket 进程间通信

          3.2 怎么实现进程通信

          notion image

          3.2.1 管道(pipe)

          • 普通 pipe : 单工,只能在父子或兄弟进程间使用
          • 流管道 s_pipe : 半双工,只能在父子或兄弟进程间使用
          • 有名管道 name_pipe(FIFO) :允许在不相关进程间进行通信

          3.2.2 信号(signal)

          用于通知接收进程某个事件已经发生。
          输出结果

          3.2.3 消息队列(message queue)

          克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷。
          输出结果

          3.2.4 信号量( semophore )

          • 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源;
          • 主要作为进程间以及同一进程内不同线程之间的同步手段;
          • Linux提供了一组精心设计的信号量接口来对信号量进行操作,它们声明在头文件sys/sem.h中。

          3.2.5 共享内存( shared memory )

          共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
          输出结果:

          3.2.6 套接字( socket )

          • 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信
          notion image
          notion image

          引用

          CPU眼里的:进程、线程 | MMU系列 | 空间独立性 - 阿布的视频 - 知乎 https://www.zhihu.com/zvideo/1469706912229429248
          MMU 的视角,重新认识线程和进程,了解“空间独立性”的基本硬件原理和使用场景
          网络服务器更多使用多进程,很少使用多线程
          上一篇
          Signal 信号处理与 IO 复用
          下一篇
          多线程、并发与线程同步

          Comments
          Loading...