深入Linux(三):Linux进程间通信和同异步机制

Linux进程间通信和同异步机制

目录

进程或线程持有锁期间终止了,怎么办?
  1. 设置PTHREAD_MUTEX_ROBUST属性
  2. 利用RAII思想,可以使用RAII的设计方法, 离开作用域的时候栈对象析构, 从而释放锁。比如使用try-finally语句块确保锁的释放:在加锁和解锁的代码块中使用try-finally语句块,以确保在任何情况下都会释放锁。这样即使出现异常或者程序意外退出,也能够保证锁能够被释放。
  3. 使用定时锁:在使用锁时,可以使用定时锁,它会在一定时间内自动释放锁。这种锁可以避免在锁被持有的过长时间内阻塞其他线程的执行。
进程管理和调度
  • 见OS.md
线程模型
  • 一比一模型: 每个用户线程对应一个内核线程,该模型在一个线程执行阻塞系统调用时,能够允许另一个线程继续执行,所以对于IO密集型的场景,CPU、磁盘IO、网卡等资源利用率还是非常友好的,提供了更好的并发功能

在这里插入图片描述

  • 多对一模型:多个用户线程对应一个内核线程,该模型的线程管理是由用户空间的线程库来完成的,因此效率更高,并且高效的上下文切换和几乎无限制的线程数量.不过,如果一个线程执行阻塞系统调用,那么整个进程将会阻塞.再者,因为任一时间只有一个线程可以访问内核,所以多个用户线程不能并行运行在多处理核系统上
  • 多对多模型: 多个用户线程对应多个内核线程,使得库和操作系统都可以管理线程,用户线程由运行时库调度器管理,内核线程由操作系统调度器管理,可运行的用户线程由运行时库分派并标记为准备好执行的可用线程,操作系统选择用户线程并将它映射到可用内核线程.
Linux 线程模型的比较:LinuxThreads 和 NPTL(从Linux 2.6版本开始,NPTL成为了Linux默认的线程库)
  • LinuxThread是Linux早期版本中的线程库,是基于轻量级进程(LWP)实现的,而NPTL是从Linux 2.6版本开始引入的,是基于内核线程实现的。由于NPTL是基于内核线程实现的,因此在性能上要比LinuxThread更加优秀。 LinuxThread中的一些系统调用(如gethostbyname)是非线程安全的,因此需要使用特殊的函数调用来保证线程安全性,而NPTL使用了更加高效的线程安全实现方式,因此无需使用特殊函数来保证线程安全性。
  • LinuxThreads : 线程与传统的多任务进程 之间的区别在于:线程共享的是单个进程的状态信息,并会直接共享内存和其他资源。同一个进程中线程之间的上下文切换通常要比进程之间的上下文切换速度更快。因此,多线程程序的优点就是它可以比多进程应用程序的执行速度更快。另外,使用线程我们可以实现并行处理。这些相对于基于进程的方法所具有的优点推动了 LinuxThreads 的实现
  • NPTL,或称为 Native POSIX Thread Library , NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合 POSIX 的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。与 LinuxThreads 一样,NPTL 也实现了一对一的模型。
  • 问题:
    1. NPTL模式, 线程的栈空间是从进程的栈空间里划分的嘛 ?

      答: 在NPTL模式下,线程的栈空间通常是从进程的虚拟地址空间中划分出来的,但它们是独立的,不会相互干扰。每个线程都有自己的栈空间,这些栈空间在进程的虚拟地址空间中是连续的,但它们是独立的。具体来说,当一个线程被创建时,NPTL会分配一个连续的地址空间给它作为栈空间。这个地址空间通常是从进程的虚拟地址空间中的一段空闲区域中划分出来的,但它与进程的栈空间是独立的。这意味着,每个线程的栈空间与进程的栈空间是分开的,它们不会相互干扰。值得注意的是,NPTL采用了一些特殊的机制来保护线程的栈空间,防止栈空间溢出等问题。例如,NPTL可以通过设置一个特殊的“警戒区”来检测栈空间溢出,以便及时地捕获和处理这种情况。因此,尽管线程的栈空间是从进程的虚拟地址空间中划分出来的,但它们是独立的,且具有一定的安全性保障。

System V 和 Posix 的区别(只需关注Posix即可, Posix其实是一种标准)
  • 两种实现进程间通信的标准。历史:UNIX两大贡献者贝尔实验室和BSD,在进程之间通信侧重不同,前者基于内核对进程之间的通信手段进行了改进,形成了“System V IPC”,而后者则是基于网络形成了套接字。而POSIX则是IEEE制定的标准,目的是为运行在不同操作系统上的软件提供统一的接口,实现者则是不同的操作系统内核开发人员。主要的几点区别如下:
    • 在信号量这种常用的同步互斥手段方面,POSIX在无竞争条件下是不会陷入内核的,而SYSTEM V则是无论何时都要陷入内核,因此性能稍差
    • 多线程应使用Posix
    • POSIX的sem_wait函数成功获取信号量后,进程如果意外终止,将无法释放信号量,而System V则提供了SEM_UNDO选项来解决这个问题。因此,相比而言,后者更加可靠
    • 可能有小部分操作系统没有实现POSIX标准,System V更加广泛些,但是考虑到可移植性POSIX必然是一个趋势。在IPC,进程间的消息传递和同步上,似乎POSIX用得较普遍,而在共享内存方面,POSIX实现尚未完善,system V仍为主流
进程简介
  • 进程的状态:Runnable®、Sleeping(S)、Zombie(Z)、Stopped(T)等
  • 每个进程都会被分配一个唯一的数字编号,即PID或进程标识符
进程创建
  • 启动进程的三种方式system, fork, exec 其中system不是理想手段, 进程的创建尽量使用execl,进程内非继承文件描述符需要设置FD_CLOEXEC属性

  • 启动新进程可通过库函数system来完成,system函数的作用是,运行以字符串参数的形式传递给它的命令并等待该命令的完成,ystem()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,阻塞当前进程直到command命令执行完毕,此命令执行完后随即返回原调用的进程。 具体见代码 system1.c

  • 替换进程映像通过exec系列函数,exec函数可以把当前进程替换为一个新进程 ,你可以使用exec函数将程序的执行从一个程序切换到另一个程序 。具体见代码:pexec.c

  • 复制进程映像fork(),图示和代码如下
    代码见fork1.cpid_t pid; pid = fork(); if(pid==0) fork的返回值,On success, the PID of the child process is returned in the parent, and 0 is returned in the child

    在这里插入图片描述

    • 获取父进程的pid通过getppid()来获取
    • 等待一个进程:当用fork启动一个子进程时, 子进程就有了它自己的生命周期并将独立运行。 有时,我们希望知道一个子进程何时结束。 例如, 在前面的示例程序中, 父进程在子进程之前结束, 由于子进程还在继续运行, 所以得到的输出结果有点乱。 我们可以通过在父进程中调用wait函数让父进程等待子进程的结束. wait.c
僵尸进程
  • 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
  • 如果父进程比子进程提前结束, 那么当子进程终止后, 子进程将自动把PID为1的进程( 即init) 作为自己的父进程。 子进程现在是一个不再运行的僵尸进程, 因为其父进程异常终止, 所以它由init进程接管。 应该尽量避免产生僵尸进程, 因为在init清理它们之前(前提是父进程终止了,不然init不会接管), 它们将一直消耗系统的资源。
  • 僵尸进程要变成孤儿进程后才会被init清理
  • unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
    孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
      任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
  • 僵尸进程危害场景: 例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。
进程间协作——信号
  • 标题中的信号不是信号量
  • 发送信号给进程
    • 如果向发送一个信号给进程,而该进程并不是当前的前台进程,就需要使用kill命令,使用命令如下:kill -HUP 512 该命令表示向运行在另一个终端上的PID为512的进程发送“挂断”信号。进程还可以通过kill函数来发送信号,函数原型为 int kill (pid_t pid, int sig);
    • alarm函数用于seconds秒之后安排发送一个信号,函数原型为unsigned int alarm(unsigned int seconds);
    • pause() 函数的作用为把程序的执行挂起知道有一个信号出现为止。
  • 接收信号并处理信号:对于某些信号,如果进程接受到了但事先没有安排捕获它,进程将会立即终止,信号的名称在signal.h中定义,例:SIGINT 表示终端中断,通常是CTRL+C组合键。一般不推荐使用signal,推荐使用sigaction函数,两者代码如下: ctrlc1.c ctrlc2.c
进程间通信——管道
  • 通过popen和pclose在两个程序间传递数据(popen里只能传递命令)
    • 如果open_mode是“r”, 被调用程序的输出就可以被调用程序使用, 调用程序利用popen函数返回的FILE*文件流指针, 就可以通过常用的stdio库函数( 如fread) 来读取被调用程序的输出。 如果open_mode是“w”, 调用程序就可以用fwrite调用向被调用程序发送数据, 而被调用程序可以在自己的标准输入上读取这些数据。 被调用的程序通常不会意识到自己正在从另一个进程读取数据, 它只是在标准输入流上读取数据, 然后做出相应的操作 popen1.c popen2.c popen3.c
  • 通过底层的pipe函数
    • pipe函数原型为 #include<unistd.h> int pipe(int file_descriptor[2]);写到file_descriptor[1]的数据都可以从file_descriptor[0]读回来,数据基于FIFO原则,可以理解为file_descriptor[1]是管道的写接口,file_descriptor[0]是管道的读接口 pipe1.c pipe2.c
    • 通过fork、execl和pipe实现生产者和消费者,子进程通过调用execl替换成另一个程序当消费者,从file_descriptor[0]读数据,父进程当生产者,写数据到file_descriptor[1] pipe3.c pipe4.c
    • 将管道用作标准输入输出,下面的程序相当于将管道文件描述符指向标准输入输出,这样相当于通过标准输入输出来交换数据 pipe5.c
  • 命名管道FIFO 之前的popen和pipe,只能在相关的程序之间传递数据, 即这些程序是由一个共同的祖先进程启动的。 但如果我们想在不相关的进程之间交换数据, 这还不是很方便,命名管道FIFO可以解决这个问题。
    • 创建命名管道:创建后就可以read和write了 fifo1.c
    • 使用open打开FIFO文件(对一个空的、 阻塞的FIFO( 即没有用O_NONBLOCK标志打开) 的read调用将等待,直到有数据可以读时才继续执行。 与此相反, 对一个空的、 非阻塞的FIFO的read调用将立刻返回0字节。对一个完全阻塞FIFO的write调用将等待, 直到数据可以被写入时才继续执行,这实现了某种类似信号量的作用,从而能够在不同进程间交换数据) fifo2.c
    • 使用FIFO实现进程间通信(两个程序使用的都是阻塞模式的FIFO。 我们首先启动fifo3( 写进程/生产者) , 它将阻塞以等待读进程打开这个FIFO。 fifo4( 消费者) 启动以后, 写进程解除阻塞并开始向管道写数据。 同时, 读进程也开始从管道中读取数据)fifo3.c fifo4.c
    • 使用FIFO的客户/服务应用程序(程序中有两个FIFO文件,client发送数据和接受数据是通过不同的FIFO文件) server.c client.c
文件上锁(用于进程间)
  • 针对无亲缘关系的进程读取文件时,需要进行互斥,线程中有互斥量可以使用,而进程间可以使用fcntl记录上锁
  • 具体查看lockmain.c, 详见UXIX网络编程第9章
POSIX线程
  • 一个简单的线程程序thread1.c
  • 部分pthread函数介绍
    • pthread_join用于等待一个给定进程终止;
    • pthread_self用于获取自身的线程ID;
    • pthread_detach将指定的线程转换为脱离状态,将该线程分离后,此线程结束后,会自动释放其所占资源。一般来说,如果没有用pthread_detach分离的线程,都会使用ptread_join来回收其线程所占资源
    • pthread_kill, 发送信号给指定线程,可以通过发送信号0来检测线程是否存在(详见man pthread_kill)。
  • 同步
    • 信号量: 用信号量进行同步(这里的信号量是取自POSIX的实时扩展,进程间所用的信号量是称为系统V信号量):thread3.c
    • 互斥量: 用互斥量进行同步:thread4.c ,尽量不用互斥量来同步,应该使用信号量和条件变量同步
    • 互斥量于信号量的区别: 互斥量用于线程的互斥,信号量用于线程的同步,这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别
    • 条件变量: pthread_cond_waitpthread_cond_signal,调用pthread_cond_wait会释放锁并沉睡,pthread_cond_signal会唤醒沉睡,pthread_cond_wait从沉睡中醒来,然后会重新获取锁,只有获取锁成功了,才可以执行pthread_cond_wait的下一条语句。典型的例子是生产者-消费者模式,见程序生产-消费者(条件变量).c
    • 读写锁 : 相比互斥量,读写锁在多个线程读数据时不需要加锁,只在某个写入者修改数据期间保护该数据,以免其它读出者或写入者的干扰。通过互斥锁和条件变量实现的读写锁的部分代码见读写锁.zip,详见UNIX网络编程卷二第8章读写锁
  • 线程的属性(略)
  • 取消线程 thread7.c
  • 多线程 thread8.c
信号量、共享内存和消息队列
  • 三者都用于进程间通信,由于这些机制都出现在同一个版本中并且有着相似的编程接口, 所以它们又常被称为IPC( Inter-Process Communication, 进程间通信) 机制
消息队列
  • 消息队列和之前讨论过的管道和FIFO的区别,主要有以下两点:一个进程向消息队列写入消息之前,并不需要某个进程在该队列上等待该消息的到达,而管道和FIFO是相反的,进程向其中写消息时,管道和FIFO必需已经打开来读,那么内核会产生SIGPIPE信号(感谢shanshan_fangfang的指正)。IPC的持续性不同。管道和FIFO是随进程的持续性,当管道和FIFO最后一次关闭发生时,仍在管道和FIFO中的数据会被丢弃。消息队列是随内核的持续性,即一个进程向消息队列写入消息后,然后终止,另外一个进程可以在以后某个时刻打开该队列读取消息。只要内核没有重新自举,消息队列没有被删除
  • POSIX消息队列
    • mq_open()函数打开或创建一个posix消息队列; mq_close()函数关闭消息队列 ; mq_unlink()函数从系统中删除某个消息队列; mq_setattr()函数和mq_getattr()函数分别设置和和获取消息队列属性; mq_send() 函数 和mq_receive()函数分别用于向消息队列放置和取走消息,mq_receive每次被调用时总是返回最高优先级的最早消息
    • mq_close 和 mq_unlink的区别
      别**
      - mq_close函数功能是关闭已打开的消息队列,关闭后调用进程不可以再使用该描述符,但其消息队列并没有被删除(需要调用mq_unlink)。一个进程终止时,它的所有打开着的消息队列都关闭,就像调用了mq_close一样。
      • mq_unlink函数功能是从系统中删除名为name的消息队列,但要求该名称是文件的最后一个链接,并且该文件已关闭,才会将该文件真正从磁盘上删除。即如果该详细队列的文件还在其他进程中打开,那么不会将其从磁盘上删除,又或者这是最后一个链接,但它还未关闭,即未执行mq_close操作,它也不会从磁盘上删除。
    • mq_notify : Posix消息队列允许异步事件通知,以告知何时有一个消息放置到了某个空(注意是空,已经有了就不算了)消息队列中。这种通知有两种方式可供选择:(1)产生一个信号,(2)创建一个线程来执行指定的函数。int mq_notify(mqd_t mqd, const struct sigevent *notification) 在notification中指定是产生某个信号还是执行某个线程, 详见UXIX网络编程卷二5.6节
    • 当消息的数量达到给定的最大值时,将不能再通过mq_send往该详细队列中发送消息,继续发送会阻塞或失败。
  • System V消息队列 略 程序见 msg1.c msg2.c
信号量
  • 可用于进程间或线程间同步, 分三种:Posix有名信号量,SystemV信号量,Posix基于内存的信号量(放在共享内存中), 信号量的定义可参考OS.md
  • Posix信号量
    • POSIX信号量有两种形式可供选用:有名和无名。它们的区别在于,如何被创建和销毁,其他方面则完全相同。无名信号量只存在于内存中,并且规定能够访问该内存的进程才能够使用该内存中的信号量。这就意味着,无名信号量只能被这样两种线程使用:(1)来自同一进程的各个线程(2)来自不同进程的各个线程,但是这些进程映射了相同的内存范围到自己的地址空间。相反,有名信号量则是通过名字访问,因此,来自于任何进程的线程,只要知道该有名信号量的名字都可以访问。
    • 有名信号量
      • 调用sem_open函数可以创建一个新的有名信号量,或打开一个现存的有名信号量。这里可以将信号量初始化为一个0到SEM_VALUE_MAX(这个常值至少为32767)之间任意值。
      • 调用sem_close函数释放与信号量相关的资源。
      • sem_unlink函数移除信号量的名字。如果当前没有打开的对该信号量的引用,那么就销毁它。否则,销毁被推迟到最后一个打开的引用被关闭。
      • 调用sem_wait或sem_trywait函数,请求一个信号量(对信号量值执行减1操作),如果信号量计数为0,这时如果调用sem_wait函数,将会阻塞。直到成功对信号量计数减1或被一个信号中断,sem_wait函数才会返回。我们可以使用sem_trywait函数以避免阻塞。当我们调用sem_trywait函数时,如果信号量计数为0,sem_trywait会返回-1,并将errno设置为EAGAIN。第三种方法是可以阻塞一段有限的时间,这时我们使用sem_timedwait函数
      • 可以调用sem_getvalue函数来获取信号量值
    • 无名信号量
      • 果我们想要在一个单一进程内使用POSIX信号量,那么使用无名信号量会更加简单。无名信号量只是创建和销毁有所改变,其他完全和有名信号量一样。
      • 调用sem_init函数创建一个无名信号量,这里可以将信号量初始化为一个0到SEM_VALUE_MAX(这个常值至少为32767)之间任意值。
      • 调用sem_destroy函数来销毁用完的无名信号量。调用sem_destroy后我们将不能再以sem为参数调用任何信号量函数,除非我们再次使用sem_init对sem进行初始化
      • 有名信号量的其他的函数如sem_post等和无名信号量的是通用的
  • System V信号变量代码见 sem1.c
  • 利用无名信号量实现生产者-消费者功能见 生产-消费者(信号量).c
  • 利用无名信号量实现多生产者-单消费者功能见 多生产者-单消费者.c
  • 利用无名信号量实现多生产者-多消费者功能见 多生产者-多消费者.c
  • 利用无名信号量实现的多缓冲区同步功能见 多缓冲区.c
共享内存
  • 基础
    • 共享内存是可用IPC(进程间通信)形式中最快的,因为共享内存区中的单个数据副本对于共享该内存的所有线程和进程都是可用的,省去了拷贝操作。共享内存允许两个不相关的进程访问同一个逻辑内存,共享内存是在两个正在运行的进程之间传递数据的一种非常有效的方式。共享内存是由IPC为进程创建的一个特殊的地址范围, 它将出现在该进程的地址空间中。 其他进程可以将同一段共享内存连接到它们自己的地址空间中。 所有进程都可以访问共享内存中的地址, 就好像它们是由malloc分配的一样。 如果某个进程向共享内存写入了数据, 所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。共享内存为在多个进程之间共享和传递数据提供了一种有效的方式。 由于它并未提供同步机制, 所以我们通常需要用其他的机制来同步对共享内存的访问。
    • 共享内存只用于进程间, 线程间本来各自的地址都是对其他线程可见的,即任何一段内存,其他线程都是可以直接访问的,用不到共享内存。
  • 内存映射文件 , open打开,由mmap把得到的描述符映射到当前进程地址空间中的一个文件。可用于父子间进程共享,也可用于无亲缘关系的进程间共享。
    • mmap 参数flags可以为MAP_SHARED和MAP_PRIVATE、MAP_FIXED, MAP_SHARED表示调用进程对被映射数据的修改对所有共享该内存的进程都是可见的,且会改变原文件,而MAP_PRIVATE仅对当前进程可见,且不会改变原文件。该函数用于以下三种用途:
      1. 可以使用普通文件以提供内存映射, 这样可以使得将操作文件(read,write操作)转化为操作内存区里的地址。可用于文件复制,见fileCopy.cpp.
        • 常规文件系统操作(调用read/fread等类函数)中,函数的调用过程:1.进程发起读文件请求。2.内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode。3.inode在address_space上查找要请求的文件页是否已经缓存在页缓存中。如果存在,则直接返回这片文件页的内容。4.如果不存在,则通过inode定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页面过程,进而将页缓存中的数据发给用户进程。
        • 总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。
      2. 使用特殊文件以提供匿名内存映射(不需要创建文件或者直接打开/dev/zero),实例见incr2.c, 一个父子进程间使用共享内存区的示例
      3. posix共享内存提供映射,即要使用posix共享内存区需要使用该函数来将共享内存区映射到进程的地址空间。在Linux中,POSIX共享内存是通过挂载在/dev/shm下的tmpfs内存文件系统实现的,创建的每一个共享内存都对应tmpfs中的一个文件,因此POSIX共享内存也可视为共享文件映射。
      4. munmap 删除一个映射关系
      5. msync 如果是MAP_SHARED,则内核的虚拟内存算法保持内存映射文件与内存映射区的同步,但如果我们希望确保一致,可以通过调用msync实现
      6. 深入理解内存映射mmap
  • Posix共享内存
    • POSIX共享内存实际上毫无新意,它本质上就是mmap对文件的共享方式映射,只不过映射的是tmpfs文件系统上的文件。什么是tmpfs?Linux提供一种“临时”文件系统叫做tmpfs,它可以将内存的一部分空间拿来当做文件系统使用,使内存空间可以当做目录文件来用。现在绝大多数Linux系统都有一个叫做/dev/shm的tmpfs目录,就是这样一种存在。具体使用方法,大家可以参考我的另一篇文章《Linux内存中的Cache真的能被回收么?》。Linux提供的POSIX共享内存,实际上就是在/dev/shm下创建一个文件,并将其mmap之后映射其内存地址即可。我们通过它给定的一套参数就能猜到它的主要函数shm_open无非就是open系统调用的一个封装
    • mmap匿名共享内存、XSI共享内存和POSIX共享内存在内核实现本质上其实都是tmpfs
    • 由shm_open打开一个IPC名字,所返回的描述符由mmap函数映射到当前进程的地址空间。可用于父子间进程共享,也可用于无亲缘关系的进程间共享。
    • 代码见 shm_posix.c
  • System V共享内存, 与osix共享内存类似,不具体介绍,代码见shm1.c shm2.c
其它
线程池的思想
  • 空闲时让线程等待,工作时,给空闲线程发信号,让其工作,可通过信号量semaphore来实现; 可给每个线程分配一个包含所有信息的结构体和序号,唤醒某个线程时,就将信息结构体中的函数指针和函数赋值,并调用信号量,将其加1,则线程就会运行.
  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值