- 系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。
- 相关函数
- 所需头文件:
<sys/types.h>
、<unistd.h>
- 函数原型:
pid_t fork(void);
- 返回值:返回两次,一次返回至父进程(返回子进程的ID,返回-1时则说明创建子进程失败),一次在子进程中返回(返回0). 通过fork的返回值即可区分父进程和子进程。
创建子进程后,进程的虚拟空间中的内容是一样的,但后续是相对独立的。当对其中一个进程进行写入时,即修改进程的虚拟内存空间时,二者互不影响。
进程创建失败的原因:- 系统进程数达上线,errno置EAGAIN;
- 系统内存不足,errno置ENOMEM
Linux的fork()采用写时拷贝,当调用fork函数时,并不是对整个地址空间进行复制,而是让父子进程共享同一个地址空间,当需要写入的时候才复制地址空间。fork产生的子进程与父进程相同的文件描述符指向相同的文件表,引用计数增加,共享指针偏移。
- 所需头文件:
- GDB调试默认只能跟踪一个进程,但可以在调用fork函数之前通过指令设置GDB调试跟踪的对象,默认是跟踪父进程。
- 设置调试父进程或子进程:
set follow-fork-mode [parent(默认)|child]
- 设置调试模式:
set detach-on-fork [on|off]
(默认为on,表示调试当前进程时其他进程继续运行,否则其他进程被挂起) - 查看调试的进程:
info inferiors
- 切换当前调试的进程:
inferior id
- 使进程脱离GDB调试:
detach inferiors id
Unbuntu8版本上使用
set detach-on-fork off
会有bug。
- exec函数族
- 头文件:
<unistd.h>
- 作用:根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
- 返回值:exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样。只有调用失败了,它们才会返回
-1
,从原程序的调用点接着往下执行。 - 函数原型
int execl(const char *path, const char *arg, .../* (char *) NULL */);
- 函数参数:
- path: 指定执行文件的路径或名称,推荐使用绝对路径
- arg: 执行可执行文件所需要的参数列表,第一个参数没作用,一般写执行的程序名;第二个开始往后为程序执行所需要的参数列表,参数必须以NULL结尾.
- 函数参数:
int execlp(const char *file, const char *arg, .../* (char *) NULL */);
- 函数参数:
- file: 需要执行的可执行文件的文件名
- arg: 执行可执行文件所需要的参数列表,第一个参数没作用,一般写执行的程序名;第二个开始往后为程序执行所需要的参数列表,参数必须以NULL结尾.
- 函数参数:
二者不同的使,
execlp
是在环境变量中寻找可执行文件,如果找到了就执行,否则就执行失败,因而不需要为其指定路径;而execl
则是到指定的路径中执行相应的文件,因而需要指定路径。
- 孤儿进程
对于父进程,应该对其子进程的资源进行回收,而当子进程还没执行完成,而父进程已经结束并被回收,那么此时子进程就称为孤儿进程,这种情况下子进程的父进程将会由操作系统设置为init
,由init
来回收其资源。孤儿进程没有危害。
- 僵尸进程
子进程提前结束,其用户区可以自行释放,而内核区需要父进程来释放。而当父进程未对其子进程进行释放,那么该子进程就会成为僵尸进程。由于进程号是有限,因而如果存在大量僵尸进程,可能导致系统进程号不够用导致后续的程序执行出错,这也是僵尸进程的危害。
解决方法:父进程调用wait()
或waitpid()
去释放子进程的内核区,或是直接杀死父进程(让僵尸进程转换为孤儿进程)。
- 进程退出
- 函数原型:
void exit(int status);
(标准C库),void _exit(int status);
(Linux)- 所需头文件:
<unistd.h>
- 函数参数: status为进程退出时的一个状态信息,父进程回收子进程资源时可以获取到该标志。
标准C库中的
exit()
是在_exit()
的基础上加上了一些额外的额操作,比如刷新输入输出缓冲区以及调用退出处理函数。
- 所需头文件:
- 进程回收
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
如果子进程仍未结束,那么wait会一直等待直至子进程结束后才返回,而waitpid则直接返回,不论子进程是否结束。
- 头文件:
<sys/types.h>
,<sys/wait.h>
- 函数原型:
pid_t wait(int *wstatus);
- 功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源
- 参数:wstatus为进程退出时的状态信息,传入的是地址(传出参数)
- 返回值:成功返回被回收的子进程的id,失败返回
-1
- 函数原型:
pid_t waitpid(pid_t pid, int *wstatus, int options);
- 功能:回收指定进程号的资源
- 参数:
- pid:
- pid > 0 : 某个子进程的pid
- pid = 0 : 回收当前进程组的所有子进程
- pid = -1 : 回收所有的子进程,相当于wait()
- pid < -1 : 回收某个进程组的组id的负数
- wstatus: 进程退出时的状态信息,传入的是地址(传出参数)
- options: 设置阻塞或非阻塞
- 0:阻塞
- WNOHANG: 非阻塞
- 返回值:
- > 0: 返回子进程的pid
- = 0: 还有子进程或者
- -1: 错误,或者没有子进程
- 进程间通信
进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC: Inter Processes Communication ),以实现数据传输、通知事件、资源共享以及进程控制。
8.1 匿名(无名)管道:Unix系统IPC的最古老方式,所有Unix系统都支持该通信机制。
特点:
- 在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的
- 拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据. 匿名管道用于有联系的进程(具有相同祖先进程),而有名管道则用于无联系的进程。
- 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
- 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺
序是完全一样的。 - 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
- 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写
更多的数据,在管道中无法使用 lseek() 来随机的访问数据。 - 相关函数
- 函数1:
int pipe(int pipefd[2]);
- 参数:pipefd[2]为一个传出参数,pipefd[0]为读端,pipefd[1]为写端,实际上都是文件描述符来的。
- 返回值:成功返回0,失败返回-1
- 所需头文件:
<unistd.h>
- 功能:创建一个匿名管道,用来进程间通信
- 注意:管道构建应该在fork之前,且管道默认是阻塞的。大小默认是4K。
- 函数2:
long fpathconf(int fd, int name);
- 参数:fd为文件描述符,name为配置选项(获取缓冲区大小为
_PC_PIPE_BUF
). - 头文件:
<unistd.h>
- 功能:获取缓冲区大小
- 参数:fd为文件描述符,name为配置选项(获取缓冲区大小为
- 函数1:
读写特点
- 读
- 如果写端全关,则管道中剩余的字符读取完成后,继续调用read会返回
0
,表示管道中的数据已经读取完成。 - 如果写端没有全部关闭,且写端进程没有向管道中写入数据,那么重复调用
read
后会阻塞直至管道中有数据可读才读取数据并进行返回。
- 如果写端全关,则管道中剩余的字符读取完成后,继续调用read会返回
- 写
- 如果读端全关,如果写端进程一直写入数据,当管道容量被用完时进程会收到
SIGPIPE
信号,通常会导致进程异常终止。 - 如果读端没有全部关闭,则当管道写满时,再调用
write
将会阻塞直到管道中有空数据。
- 如果读端全关,如果写端进程一直写入数据,当管道容量被用完时进程会收到
- 读
修改管道为非阻塞模式:通过
fcntl
函数修改读端的flags.
8.2 有名管道(FIFO)
有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了(如read()、 write()和close())。与管道一样, FIFO 也有一
个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。 FIFO 的名称也由此而来:先入先出。
二者的区别
- FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。
- 当使用 FIFO 的进程退出后, FIFO 文件将继续保存在文件系统中以便以后使用。
- FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。
- 创建文件(也可通过命令
mkfifo filename
来创建)- 函数原型:
int mkfifo(const char *pathname, mode_t mode);
- 头文件:
<sys/types.h>
,<sys/stat.h>
- 函数参数:
- pathname: 管道的名称及路径
- mode: 文件权限,和open的权限的一样的。
- 函数原型:
8.3 内存映射
将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件. 这里的内存指的是虚拟地址空间。
- 相关函数
- 头文件:
<sys/mman.h>
- 函数1原型:
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或设备的数据映射到内存中
- 函数参数:
- void *addr: 直接传入NULL即可,由内核创建映射首地址
- length: 要映射数据的长度,不能为0,建议使用文件的长度(lseek、stat)
- prot:
- PROT_EXEC: 执行权限
- PROT_READ: 读权限
- PROT_WRITE: 写权限
- PROT_NONE: 无权限
指定length之后,返回的地址空间不一定只有length,当length不是Linux系统规定的分页大小的整数倍时,此时地址空间的大小会向上取整到其整数倍。
要操作映射内存,必须要有读权限,因而一般设置只有读权限或者有读有写。
- flags:
- MAP_SHARED: 映射区的数据会自动和磁盘文件进行同步,进程间通信必须设置这个选项
- MAP_PRIVATE: 不同步,映射内存修改了,文件内容不会修改,会新建一个新的文件,并将数据写入该文件中(写时拷贝)
- fd:需要映射到的那个文件的描述符(文件大小不能为0,且open的权限不能和前边的prot权限有冲突)
- offset: 映射内存相对于文件头的偏移量, 必须指定为4K的整数倍
- 返回值:返回创建的映射内存首地址,失败时返回MAP_FAILED((void *) -1)
- 函数2原型:
int munmap(void *addr, size_t length);
- 功能:释放内存映射
- 函数参数:
- addr: 要释放的内存首地址
- length:要释放的内存的大小,和mmap函数的length参数值一样
有关系的进程间通信:
- 通过父进程创建内存映射区,然后创建子进程,两个进程共享该内存映射区。
没有关系的进程间通信:- 准备大小不为0的磁盘文件,进程1通过磁盘文件创建内存映射区,进程2同样获取内存映射去,然后通过该映射区进行通信。
注意:内存映射区是不阻塞的。父子进程可用通过匿名映射来实现内存映射,无需创建文件实体,只需要在flags参数那里加上|MAP_ANONYMOUS
表示该映射为匿名映射。
- 准备大小不为0的磁盘文件,进程1通过磁盘文件创建内存映射区,进程2同样获取内存映射去,然后通过该映射区进行通信。
- 通过父进程创建内存映射区,然后创建子进程,两个进程共享该内存映射区。
- 头文件:
8.4 信号
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
引发内核产生信号的事件有:硬件异常、系统状态变化、外部输入中断以及运行kill命令或kill函数。
Linux信号列表可用通过kill -l
来访问,一共有62个信号(编号从1-64,但32、33没有信号,前31种为常规信号,而后31种是可以用户自定义的)。
- 信号的五种默认处理动作:
- Term: 终止进程
- Ign:当前进程忽略该信号
- Core:终止进程,并生成一个Core文件, 在GDB调试下通过
core-file filename
可以查看core文件中的错误信息。 - Stop: 暂停当前进程
- Cont: 继续执行当前被暂停的进程
- 信号的状态:产生、未决、递达(SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。)
信号相关函数
- 函数1
- 函数原型:
int kill(pid_t pid, int sig);
- 函数功能: 给某个进程或进程组发送某个信号
- 头文件:
<sys/types.h>
,<signal.h>
- 函数参数:pid为进程pid, sig为信号宏值或编号。
- pid>0: 信号发送给某个进程
- pid=0: 信号发送给当前的进程组
- pid=-1: 信号发送给每一个有权限接受这个信号的进程
- pid<-1: 给某个信号组发信号(pid取绝对值即为pgpid)
- 返回值:
- 函数原型:
- 函数2
- 函数原型:
int raise(int sig);
- 头文件:
<signal.h>
- 功能:给当前进程发送sig信号,相当于
kill(getpid(), sig);
- 返回值:成功返回0,不成功返回非0
- 函数原型:
- 函数3
- 函数原型:
void abort(void)
- 头文件:
<signal.h>
- 功能:将SIGABRT信号给当前的进程,杀死当前进程,相当于
kill(getpid(), SIGABRT);
- 函数原型:
- 函数4
- 函数原型:
unsigned int alarm(unsigned int seconds);
- 头文件:
<unistd.h>
- 功能:设置一个定时器, 函数调用开始倒计时,当倒计时为0时,函数会给当前进程发送一个信号:SIGALARM(默认终止当前进程。每一个进程有且只有一个定时器,默认不阻塞)
- 参数: seconds为设定的定时时间。参数为0时定时器无效,取消也是传入0来实现。
- 返回值:上一个定时器倒计时剩余的时间
定时器和进程的状态无关,不论其状态如何,定时器都是计时。
- 函数原型:
函数5
- 函数原型:
int setitimer(int which, const struct itimerval *new_val,struct itimerval *old_value);
- 头文件:
<sys/time.h>
- 作用:实现周期性计时,当计时完成后会自动重装计数值。精度比alarm高,为微秒级。
参数:
- which: 定时器以那种类型时间进行计时
- ITIMER_REAL: 真实时间,时间到达发送
SIGALARM
信号 - ITIMER_VIRTUAL:用户时间,发送
SIGVTALRM
信号 - ITIMER_PROF:以该进程在用户态和内核态所消耗的时间,发送
SIGPROF
信号
- ITIMER_REAL: 真实时间,时间到达发送
new_val: struct inimerval结构体指针,设置定时器属性
struct itimerval { // 定时器结构体 struct timeval it_interval; // 每个阶段的时间 struct timeval it_value; // 延迟多久执行定时器 }; struct timeval { // 时间结构体 time_t tv_sec; // 秒数 suseconds_t tv_usec; // 微秒 };
old_value:struct inimerval结构体指针,记录上次的定时器属性,一般不使用
- which: 定时器以那种类型时间进行计时
- 返回值:成功为0,失败为-1.
- 函数原型:
- 函数1
8.5 信号集
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为“未决信号集” 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我
们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。(类似于类的封装)
阻塞信号集可以进行修改,而未决信号机不能修改(但可以读取)。
信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
当进程产生了某个信号而尚且未被处理时,内核将其存储到未决信号集中(为1说明未决,为0说明不是未决),当该信号处理之前,内核会将未决信号集和阻塞信号集进行比较,如果该位阻塞,则信号继续处于未决直至阻塞解除,否则信号就被处理。
- 清空信号集
- 函数原型:
int sigemptyset(sigset_t *set);
- 头文件:
<signal.h>
- 功能:将信号集的标志位全部清零
- 参数:set,传出参数,需要操作的信号集
- 返回值: 成功返回0, 失败返回-1
- 函数原型:
- 置位信号集
- 函数原型:
int sigfillset(sigset_t *set);
- 头文件:
<signal.h>
- 功能:将信号集的标志位全部置为1
- 参数:set,传出参数,需要操作的信号集
- 返回值: 成功返回0, 失败返回-1
- 函数原型:
- 设置信号阻塞
- 函数原型:
int sigaddset(sigset_t *set, int signum);
- 头文件:
<signal.h>
- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
- 参数:set,传出参数,需要操作的信号集
- 返回值: 成功返回0, 失败返回-1
- 函数原型:
- 设置信号非阻塞
- 函数原型:
sigdelset(sigset_t *set, int signum);
- 头文件:
<signal.h>
- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
- 参数:set,传出参数,需要操作的信号集
- 返回值: 成功返回0, 失败返回-1
- 函数原型:
- 判断信号是否阻塞
- 函数原型:
int sigismember(const sigset_t *set, int signum);
- 头文件:
<signal.h>
- 功能:判断某个信号是否阻塞
- 参数:set,需要操作的信号集;signum为判断的信号
- 返回值: 阻塞为1, 不阻塞为0, 错误为-1
- 函数原型:
以上函数均用于自定义信号集。以下的函数才是对内核的信号集进行操作。
- 设置阻塞信号集
- 函数原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 头文件:
<signal.h>
- 功能:将自定义的信号集中的数据设置到内核中
- 参数:
- how:如何对内核阻塞信号集进行处理
- SIGBLOCK: 用户信号集添加到内核中,内核中的其他数据不变, mask|set
- SIGUNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞,mask &= ~set
- SIGSETMASK: 覆盖内核中原来的值,mask = set
- set: 用户定义信号集
- oldset: 保存设置之前的内核中的阻塞信号集状态,可以设为NULL
- how:如何对内核阻塞信号集进行处理
- 返回值: 成功为0, 失败为-1(EFAULT, EINVAL)
- 函数原型:
- 读取未决信号集
- 函数原型:
int sigpending(sigset_t *set);
- 头文件:
<signal.h>
- 参数:set保存内核中的未决信号集中的信息
- 返回值: 成功为0,失败为-1.
- 函数原型:
8.6 信号捕捉
信号捕捉其实很接近单片机中的中断,当执行主控制流场的某个指令因为中断、异常或系统调用而进入内核,那么由内核处理完先处理当前进程可以递送的信号(调用内核中的do_signal()
), 如果该信号处理动作为用户定义的处理函数则回到用户模式执行相应的处理函数(调用void sighandler(int)
),处理结束后执行特殊的系统调用sys_sigreturn()
再次进入内核,然后再返回用户模式继续从上次中断的地方继续往下执行。
- 函数1
- 函数原型:
sighandler_t signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为
- 头文件:
<signal.h>
- 参数:
- signum: 要捕捉的信号
- handler: 函数指针,表明捕捉到信号后要如何处理
- SIGIGN: 忽略信号
- SIG_DFL: 信号的默认行为
- 回调函数:由内核(系统)调用
- 返回值:成功则返回上一次注册的信号处理函数的地址,第一次返回NULL; 失败则返回
SIGERR
- 函数原型:
- 函数2
- 函数原型:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
- 功能:修改信号的执行行为
- 头文件:
<signal.h>
- 参数:
- signum: 所要捕捉的信号,但是
SIGKILL
和SIGSTOP
除外 - act: 捕捉到信号之后的处理动作
- sa_handler:
- SIGDFL: 执行默认行为
- SIGIGN:忽略该信号
- 自定义
- sa_sigaction: 不常用
- sa_mask: 临时阻塞信号集,信号捕捉函数执行过程种,临时阻塞某些信号
- sa_flags: 指定使用哪一个处理函数来处理信号,为0则使用sa_handler,为SA_SIGINFO则用sa_sigaction
- sa_restorer: 已弃用,传递NULL
- sa_handler:
- oldact: 上一次对信号捕捉的相关设置,一般不使用,传递NULL即可
- signum: 所要捕捉的信号,但是
- 返回值: 成功为0,失败为-1.
struct sigaction { void (*sa_handler)(int); // 处理函数指针 void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
- 函数原型:
常规信号不支持信号排队,而实时信号支持。
8.7 SIGCHLD信号
SIGCHLD信号产生的条件:
- 子进程终止时
- 子进程接收到 SIGSTOP 信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号.
8.8 共享内存
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
使用步骤:
- 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
- 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
- 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
- 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
- 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。
相关函数:
- int shmget(key_t key, size_t size, int shmflg);
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识,新创建的内存段的数据都会被初始化为0
- 参数
- key: key_t类型(整型),通过这个找到或创建一个共享内存,一般使用16进制,不为0
- size: 共享内存段的大小
- shmflg:属性
- 访问权限,读写权限,0664
- 附加属性:创建/判断共享内存是否存在
- IPC_CREAT:创建
- IPC_EXCL:判断是否存在,需要和IPC_CREAT一起使用
- 返回值:失败返回-1,成功返回共享内存的ID
- void shmat(int shmid, const void shmaddr, int shmflg);
- 功能:将共享内存和当前进程进行关联
- 参数:
- shmid: 通过shmget获取的共享内存ID
- shmaddr:申请共享内存的起始地址,指定NULL让内核自己决定即可
- shmflg: 对共享内存的操作
- 读:SHM_RDONLY, 必须要有读权限
- 读写:0
- 返回值:成功返回共享内存首地址,失败返回
(void *) -1
.
- int shmdt(const void *shmaddr);
- 功能:解除当前进程和共享内存的关联
- 参数:shmaddr为共享内存的首地址
- 返回值:成功为0,失败为-1
- int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能:操作共享内存,主要用于删除共享内存,共享内存需要进行删除之后才会消失
- 参数:
- shmid: 共享内存的ID
- cmd: 要做的操作
- IPC_STAT: 获取共享内存的当前状态
- IPC_SET:设置共享内存的状态
- IPC_RMID: 标记共享内存需要被销毁,但不会立刻删除,标识就是修改共享内存的键为0.
- buf: 设置/获取的共享内存的属性信息
- IPC_STAT: 存储数据
- IPC_SET: 要设置的数据
- IPC_RMID: 设置NULL即可
- key_t ftok(const char *pathname, int proj_id);
- 功能:根据指定的路径名和int值,生成一个共享内存的key
- 参数
- pathname: 指定一个存在的路径,必须要存在的且可以访问的
- proj_id: int类型,但系统只会用其中一个字节
- 返回值:成功返回key_t类型的key,失败返回-1.
共享内存和内存映射区别:
- 共享内存可以直接创建,而内存映射需要文件实体(匿名映射除外)
- 共享内存效率更高,修改的是内存而不是磁盘
- 所有进程操作的是同一个共享内存,而内存映射中每个进程都有一个独立的内存块
- 数据安全:
- 进程突然退出:共享内存还存在,而内存映射消失;远程电脑宕机,共享内存不存在,内存映射由于有文件实体因而可恢复。
- 内存映射区在进程退出时就被销毁,而共享内存需要手动标记删除,当关联数为0时才会消失。如果进程退出,会自动和共享内存取消关联。
9.