Linux进程间通信的方式:
(还有内存映射)
特点:
管道:一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
管道拥有文件的特质:读操作、写操作
一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一致的。
在管道中的数据的传递方向是单向的,一端用于写入,—端用于读取,管道是半双工的。
从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃.释放空间以便写更多的数据.在管道中无法使用 lseek() 来随机的访问数据。
匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
为什么可以使用管道进行进程间通信?
我们一开始在父进程中通过fork()创建子进程,父子进程的虚拟地址空间实际上是基于"读时共享(子进程被创建,两个进程没有做任何写的操作),写时拷贝"原则。即子进程内核区中文件描述符表是和父进程保持一致的(在不后期修改的前提下)。那么只要在fork()之前通过pipe()创建匿名管道获取对读端和写端的文件描述符,之后fork()就可以让父子进程同时获得对该管道的读/写权限,那么就可以进行进程间通信。
管道数据结构:
环形队列:空间可以重复使用;数据只能被读取一次。
三种匿名管道通信的情况:
匿名管道的使用
#include
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信
参数:
-int pipefd[2]: 是一个传出参数
pipefd[0]: 对应的是管道的读取端
pipefd[1]: 对应的是管道的写入端
返回值:
成功返回0;失败返回-1,设置errno。
管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞。
注意:匿名管道只能用于具有关系的进程之间的通信(父子进程 兄弟进程等)
eg.
int pipefd[2];
int ret = pipe(pipefd);
#include
long fpathconf(int fd, int name);
eg.
int pipefd[2];
int ret = pipe(pipefd);
// 获取管道的大小
long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
# 查看管道缓冲大小命令
boyangcao@MyLinux:~$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15407
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 15407
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
示例:父子进程分别进行数据的输出与接收(半双工通信)
#include
#include
#include
#include
#include
int main()
{
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0)
{
// 父进程
printf("i am parent process, pid: %d\n", getpid());
char buf[1024] = {0};
while(1)
{
// 从管道的读取端读取数据
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv: %s, pid : %d\n", buf, getpid());
// 向管道中写入数据
char * str = "Hello, i am parent";
write(pipefd[1], str, strlen(str));
sleep(1);//使得write完成后被另一进程抢夺CPU资源,从而读取管道数据,实现通信
}
}
else if(pid == 0)
{
// 子进程
printf("i am child process, pid: %d\n", getpid());
char buf[1024] = {0};
while(1)
{
// 向管道中写入数据
char * str = "Hello, i am child";
write(pipefd[1], str, strlen(str));
sleep(1);//使得write完成后被另一进程抢夺CPU资源,从而读取管道数据,实现通信
// 从管道的读取端读取数据
int len = read(pipefd[0], buf, sizeof(buf));
printf("child recv: %s, pid : %d\n", buf, getpid());
}
}
return 0;
}
注意:在匿名管道中进行相互通信时,有时候会出现一个进程连续进行读和写管道的操作(自己接收自己发出的信息),所以在用匿名管道通信时一般会限定其为单向通信。
即:
// 关闭读端(或者写端)
close(pipefd[0]);
示例:实现 ps aux | grep xxx 父子进程间通信
/*
实现 ps aux | grep xxx 父子进程间通信
子进程:ps aux,子进程结束后,将数据发送给父进程
父进程:获取到数据,过滤
pipe()
execlp()
子进程将标准输出 stdout_fileno 重定向到管道的写端. dup2()
*/
#include
#include
#include
#include
#include
#include
int main()
{
// 创建一个管道
int fd[2];
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0)
{
// 父进程
// 关闭写端
close(fd[1]);
// 从管道中读取数据
char buf[1024] = {0};
int len = -1;
while((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) // 留出一个字符串结束符
{
// 过滤数据并输出
printf("%s", buf);
memset(buf, 0, 1024); // 清空数组内容
}
wait(NULL);
}
else if(pid == 0)
{
// 子进程
// 关闭读端
close(fd[0]);
// 文件描述符的重定向 stdout_fileno -> fd[1]
// 执行 ps aux
dup2(fd[1], STDOUT_FILENO);
// 执行
execlp("ps", "ps", "aux", NULL);
// 执行execlp()失败返回错误
perror("execlp");
exit(0);
}
else{
perror("fork");
exit(0);
}
return 0;
}
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作):
所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0)
有进程从管道的读端读数据,管道中剩余的数据被读取以后再次read会返回0,就像读到文件末尾一样。
如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程
也没有往管道中写数据:
此时有进程从管道中读取数据,管道中剩余的数据被读取后再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0):
此时有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE,通常会导致进程异常终止。
如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程
也没有从管道中读数据:
这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。
示例:设置管道读端非阻塞
#include
#include
#include
#include
#include
#include
/*
设置管道非阻塞
int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(fd[0], F_SETFL, flags); // 设置新的flag
*/
int main()
{
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1)
{
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0)
{
// 父进程
printf("i am parent process, pid: %d\n", getpid());
// 关闭写端
close(pipefd[1]);
// 设置管道非阻塞
int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(pipefd[0], F_SETFL, flags); // 设置新的flag
char buf[1024] = {0};
while(1)
{
// 从管道的读取端读取数据
int len = read(pipefd[0], buf, sizeof(buf));
printf("len: %d\n", len);
printf("parent recv: %s, pid : %d\n", buf, getpid());
memset(buf, 0, 1024); // 清空数组内容 将数组buf中1024B设置为0
sleep(1);
}
}
else if(pid == 0)
{
// 子进程
printf("i am child process, pid: %d\n", getpid());
char buf[1024] = {0};
// 关闭读端
close(pipefd[0]);
while(1)
{
// 向管道中写入数据
char * str = "Hello, i am child";
write(pipefd[1], str, strlen(str));
sleep(5);
}
}
return 0;
}
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。
有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO相互通信,因此,通过FIFO 不相关的进程也能交换数据。
一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了(如read ()、 write()和close())。
与管道一样, FIFO也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO的名称也由此而来:先入先出。
有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于:
创建FIFO文件
1. 通过命令: mkfifo 名字
2. 通过函数: int mkfifo(const char *pathname, mode_t mode);
#include
#include
int mkfifo (const char *pathname, mode_t mode);
参数:
- pathname: 管道名称的路径 (相对/绝对路径)
- mode: 文件权限 和open的mode一致 是一个八进制数
返回值:成功返回0;失败返回-1,设置errno
eg. int ret = mkfifo("fifo1", 0664);
使用mkfifo创建一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于FIFO。如: close、read、write、unlink等。
FIFO严格遵循先进先出 (First in First out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek ( )等文件定位操作。
有名管道的注意事项:
读管道:
写管道:
示例:write.c 向管道写数据 read.c 从管道读数据
// write.c
// 向管道中写数据
#include
#include
#include
#include
#include
#include
int main()
{
// 1. 判断管道文件是否存在
int res = access("test", F_OK);
if(res == -1)
{
printf("管道不存在,创建管道\n");
// 2. 创建管道文件
int ret = mkfifo("test", 0664);
if(ret == -1)
{
perror("mkfifo");
exit(0);
}
}
// 3. 以只写的方式打开管道
int fd = open("test", O_WRONLY);
if(fd == -1)
{
perror("open");
exit(0);
}
// 4. 写数据
for(int i = 0; i < 100; i++)
{
char buf[1024];
sprintf(buf, "Hello, %d\n", i);
printf("write data: %s\n", buf);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
// read.c
// 从管道中读数据
int main()
{
// 1. 打开管道文件
int fd = open("test", O_RDONLY);
if(fd == -1)
{
perror("open");
exit(0);
}
// 读数据
while(1)
{
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
if(len == 0)
{
printf("写端断开连接...\n");
break;
}
printf("recv buf: %s\n", buf);
}
close(fd);
return 0;
}
但是上述只能实现“你说一句我回答一句”的效果,无法连续发送或接收多条信息。
解决方案:将写入管道和读取管道分为两个进程并发执行:
// chatA.c
/**/
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 1. 判断管道文件是否存在
int ret = access("fifo1",F_OK);
if(ret == -1)
{
// 文件不存在
printf("管道文件不存在,创建对应的管道文件\n");
ret = mkfifo("fifo1", 0664);
if(ret == -1)
{
perror("mkfifo");
exit(-1);
}
}
ret = access("fifo2",F_OK);
if(ret == -1)
{
// 文件不存在
printf("管道文件不存在,创建对应的管道文件\n");
ret = mkfifo("fifo2", 0664);
if(ret == -1)
{
perror("mkfifo");
exit(-1);
}
}
// 2. 开辟双进程
// 并发地写读数据
char buf[128];
pid_t pid = fork();
if(pid > 0)
{
// 是父进程:执行写操作
// 3. 以只写的方式打开fifo1
int fdw = open("fifo1", O_WRONLY);
if(fdw == -1)
{
perror("open");
exit(-1);
}
printf("打开fifo1成功, 等待写入数据...\n");
while(1)
{
memset(buf, 0, 128); // 将buf内容清空
// 获取标准输入的数据
fgets(buf, 128, stdin);
// 4. 写数据
ret = write(fdw, buf, strlen(buf));
if(ret == -1)
{
perror("write");
break;
}
}
// 5. 关闭文件描述符
close(fdw);
}
else if(pid == 0)
{
// 是子进程:执行读操作
// 3. 以只读的方式打开fifo2
int fdr = open("fifo2", O_RDONLY);
if(fdr == -1)
{
perror("open");
exit(-1);
}
printf("打开fifo2成功, 等待读取数据...\n");
while(1)
{
// 4. 读取管道数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if(ret <= 0)
{
perror("read");
break;
}
printf("B: %s\n",buf);
}
// 5. 关闭文件描述符
close(fdr);
}
return 0;
}
// chatB.c
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 1. 判断管道文件是否存在
int ret = access("fifo1",F_OK);
if(ret == -1)
{
// 文件不存在
printf("管道文件不存在,创建对应的管道文件\n");
ret = mkfifo("fifo1", 0664);
if(ret == -1)
{
perror("mkfifo");
exit(-1);
}
}
ret = access("fifo2",F_OK);
if(ret == -1)
{
// 文件不存在
printf("管道文件不存在,创建对应的管道文件\n");
ret = mkfifo("fifo2", 0664);
if(ret == -1)
{
perror("mkfifo");
exit(-1);
}
}
// 2. 开辟双进程
// 并发地写读数据
char buf[128];
pid_t pid = fork();
if(pid > 0)
{
// 是父进程:执行读操作
// 3. 以只读的方式打开fifo1
int fdr = open("fifo1", O_RDONLY);
if(fdr == -1)
{
perror("open");
exit(-1);
}
printf("打开fifo1成功, 等待读取数据...\n");
while(1)
{
// 4. 读取管道数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if(ret <= 0)
{
perror("read");
break;
}
printf("A: %s\n",buf);
}
// 5. 关闭文件描述符
close(fdr);
}
else if(pid == 0)
{
// 是子进程:执行写操作
// 3. 以只写的方式打开fifo2
int fdw = open("fifo2", O_WRONLY);
if(fdw == -1)
{
perror("open");
exit(-1);
}
printf("打开fifo2成功, 等待写入数据...\n");
while(1)
{
memset(buf, 0, 128); // 将buf内容清空
// 获取标准输入的数据
fgets(buf, 128, stdin);
// 4. 写数据
ret = write(fdw, buf, strlen(buf));
if(ret == -1)
{
perror("write");
break;
}
}
// 5. 关闭文件描述符
close(fdw);
}
return 0;
}
内存映射(Memory-mapped I/O):将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
#include
void *mmap(void *addr,size_t length, int prot, int flags,int fd,off_t offset);
int munmap (void *addr, size_t length);
#include
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
功能:将一个文件或者设备数据映射到内存中
参数:
- void* addr: (映射的起始地址由addr指定)
- NULL, 由内核选择一个地址创建映射
- length: 要映射的数据长度,这个值不能为0,建议使用文件长度。
获取文件的长度:stat() lseek()
实际数据长度为分页大小的整数倍(不足自动补全)
- prot: 对申请的内存映射区的操作权限
-PROT_EXEC Pages may be executed. 可执行权限
-PROT_READ Pages may be read. 读的权限
-PROT_WRITE Pages may be written. 写的权限
-PROT_NONE Pages may not be accessed. 没有权限
要操作映射内存,必须要有读的权限。
PROT_READ 、 PROT_READ | PROT_WRITE
- flags:
-MAP_SHARED: 映射区的数据会自动和磁盘文件进行同步,进程间通信必须设置此选项
-MAP_PRIVATE: 不同步,内存映射区的数据改变,对原来的文件不会修改,会重新创建一个新的文件(copy on write, 写时复制)
-MAP_ANONYMOUS: The mapping is not backed by any file; its contents are initialized to zero. The fd argument is ignored;(匿名映射)
- fd: 需要映射的文件的文件描述符
- 通过open()得到,open的是一个磁盘文件
- 注意:
- 文件的大小不能为0
- open指定的权限不能和prot有冲突 (prot权限一定不能大于open权限)
prot: PROT_READ open: 只读/读写
prot: PROT_READ | PROT_WRITE open: 读写
- -1: when MAP_ANONYMOUS (or MAP_ANON) is specified(匿名映射)
- offset: 映射偏移量,一般不使用 (必须指定4k的整数倍) 0表示不偏移
当为匿名映射时设置为 0
返回值:返回创建内存的首地址
-失败返回 MAP_FAILED 即 (void *) -1 并设置errno
int munmap(void *addr, size_t length);
功能:释放内存映射
参数:
- addr: 要释放的内存首地址
- length: 要释放的内存大小 和mmap()中的length大小一致
返回值:
- success, returns 0.
- failure, returns -1, and errno is set to indicate the cause of the error (probably to EINVAL).
案例:实现有关系的进程(父子进程)间通信
/*
使用内存映射实现进程间通信
1. 有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区后创建子进程
- 父子进程共享创建的内存映射区
*/
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 打开一个文件
int fd = open("text.txt", O_RDWR);
// 获取文件大小
int size = lseek(fd, 0, SEEK_END);
// 2. 创建内存映射区
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
// 3. 创建子进程
pid_t pid = fork();
if(pid > 0)
{
wait(NULL);
// 父进程
char buf[64];
strcpy(buf, (char*) ptr);
printf("read data: %s\n", buf);
}
else if(pid == 0){
// 子进程
strcpy((char *)ptr, "nihao a, son!!!");
}
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
案例:实现没有关系的进程(父子进程)间通信
/*
2. 没有关系的进程间通信
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 使用内存映射区进行通信
注意:内存映射区通信,是非阻塞。
*/
// mmap-norelationship-ipc1.c
int main()
{
// 打开一个文件
int fd = open("test.txt", O_RDWR);
// 获取文件大小
int size = lseek(fd, 0, SEEK_END);
// 2. 创建内存映射区
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
// 读取内存中数据
char buf[64];
while(1)
{
strcpy(buf, (char*) ptr);
printf("read data: %s\n", buf);
sleep(1);
memset(buf, 0, 64);
}
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
// mmap-norelationship-ipc2.c
int main()
{
// 打开一个文件
int fd = open("test.txt", O_RDWR);
// 获取文件大小
int size = lseek(fd, 0, SEEK_END);
// 2. 创建内存映射区
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
// 3. 向内存映射区写数据
char buf[64];
while(1)
{
fgets(buf, 128, stdin);
strcpy((char *)ptr, buf);
}
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
如果对mmap的返回值(ptr)做++操作(ptr++),munmap是否能够成功?
void * ptr = mmap(...);
ptr++; // 可以对其进行++操作,但是不建议
munmap(ptr, len); // 错误,要保存地址
如果open时 O_RDONLY,mmap 时 prot 参数指定 PROT_READ | PROT_WRITE 会怎样?
如果文件偏移量(offset)为1000会怎样?
偏移量必须是 4K 的整数倍,返回 MAP_FAILED
mmap 还有什么情况下会调用失败?
第二个参数:length = 0
第三个参数:
prot 只指定了写权限;
prot PROT_READ | PROT_WRITE
而第5个参数 fd 通过open函数时指定的 O_RDONLY 或者是 O_WRONLY
可以open的时候 O_CREAT 一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()
mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("XXX");
mmap(,,,,fd,0);
close(fd);
// 映射区还存在,创建映射区的fd被关闭,没有任何影响。
// mmap()对传入进来的文件描述符fd进行了拷贝(dup() / fcntl())
对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,,,,,);
// 并不是真的只分配了100B的内存用于内存映射,实际按照分页的大小(4K)进行内存分配
// 不同系统分页大小不同
// 越界操作操作的是非法的内存(野内存) -> 段错误
// 使用内存映射实现文件拷贝的功能
/*
思路:
1. 对原始的文件进行内存映射
2. 创建一个新文件(拓展该文件)
3. 把新文件的数据映射到内存中
4. 通过内存拷贝将第一个文件的内容拷贝新的文件内存中
5. 释放资源
*/
int main()
{
// 1.对原始文件进行内存映射
int fd = open("english.txt", O_RDWR);
if(fd == -1)
{
perror("open");
exit(-1);
}
// 获取原始文件的大小
int len = lseek(fd, 0, SEEK_END);
// 2. 创建一个新文件(拓展该文件)
int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
if(fd1 == -1)
{
perror("open");
exit(-1);
}
// 对新创建的文件进行拓展
truncate("cpy.txt", len);
write(fd1, " ", 1);
// 3. 分别做内存映射
void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
void* ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
if(ptr1 == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
// 内存拷贝
memcpy(ptr1, ptr, len);
// 释放资源
munmap(ptr1,len);
munmap(ptr,len);
close(fd1);
close(fd);
return 0;
}
/*
匿名映射:
不需要文件实体进行一个内存映射
只能进行父子进程这些有关系进程的映射
*/
int main()
{
// 1. 创建匿名内存映射区
int len = 4096;
void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
// 父子进程间通信
pid_t pid = fork();
if(pid > 0)
{
// 父进程
strcpy((char*) ptr, "Hello, World");
wait(NULL);
}
else if (pid == 0)
{
// 子进程
sleep(1);
printf("%s\n", (char *)ptr);
}
// 释放内存映射区
int ret = munmap(ptr, len);
if(ret == -1)
{
perror("munmap");
exit(-1);
}
return 0;
}
信号是Linux进程间通信的最古老的方式之一、是事件发生时对进程的通知机制,有时也称之为软件中断。它是在软件层次上对中断机制的一种模拟。是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
tips. 硬件中断
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
使用信号的两个主要目的是:
信号的特点:
查看系统定义的信号列表: kill -l
# 前31个信号为常规信号,其余为实时信号。(重点掌握前31个信号,后面是预定义好的信号)
boyangcao@MyLinux:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
Term # 终止进程
Ign # 当前进程忽略掉这个信号
core # 终止进程并生成一个core文件(core: 保存进程异常退出的错误信息,便于进行调试)
stop # 暂停当前进程
Cont # 继续执行当前被暂停的进程
# 输出core文件方法:设置core file size 为1024
boyangcao@MyLinux:~/Linux/Lesson26$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15407
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 1048576
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 15407
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
boyangcao@MyLinux:~/Linux/Lesson26$ ulimit -c 1024
注意:有时候指定 gcc core.c -g,再 ./a.out,不会生成 core 文件。
解决办法:使用两种方式不产生core文件是文件生产目录错误
# 查看core文件生产目录
cat /proc/sys/kernel/core_pattern
# 使用下面的命令
sudo bash -c "echo core > /proc/sys/kernel/core_pattern"
boyangcao@MyLinux:~/Linux/Lesson26$ cat /proc/sys/kernel/core_pattern
|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E
boyangcao@MyLinux:~/Linux/Lesson26$ sudo bash -c "echo core > /proc/sys/kernel/core_pattern"
boyangcao@MyLinux:~/Linux/Lesson26$ cat /proc/sys/kernel/core_pattern
core
之后在gdb中查看core文件:
boyangcao@MyLinux:~/Linux/Lesson26$ ./a.out
段错误 (核心已转储)
boyangcao@MyLinux:~/Linux/Lesson26$ gdb a.out
(gdb) core-file core
[New LWP 8377]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault. # SIGSEGV: 进程进行了无效内存访问
#0 0x000055fc55bd6602 in main () at core.c:8
8 strcpy(buf, "Hello");
信号相关函数:
int kill(pid_t pid, int sig);
int raise (int sig);
void abort (void);
#include
#include
int kill(pid_t pid, int sig);
功能:给任何的进程或者进程组pid,发送某个信号sig
参数:
- pid: 需要发送给的进程的id
> 0: 将信号发送给指定的进程
= 0: 将信号发送给当前的进程组
= -1: 将信号发送给每一个有权限接收这个信号的进程
< -1: pid = 某个进程组的id取反 (-12345 即为id=12345的进程组)
给这个进程组所有进程发送信号
- sig: 需要发送的信号的编号或者是宏值,0表示不发送任何信号
eg.
kill(getppid(), 9);
kill(getpid(), 9);
int raise (int sig);
功能:给当前进程发送信号
参数:
-sig: 需要发送的信号的编号或者是宏值,0表示不发送任何信号
返回值:
- 成功:0
- 失败:nonzero
eg.
kill(getpid(), sig) 效果等同于 raise(sig)
void abort (void);
功能:发送SIGABRT信号给当前进程,杀死当前进程
eg.
kill(getpid(), SIGABRT);
alarm():
#include
unsigned int alarm(unsigned int seconds);
功能: 设置定时器(闹钟) 函数调用,开始倒计时,
当倒计时为0的时候,函数会给当前进程发送一个信号:SIGALRM
-SIGALRM: 默认终止当前进程,每一个进程都有且只有唯一的一个定时器。
参数:
seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)
取消一个定时器: 通过alarm(0);
返回值:
-之前没有定时器:返回0
-之前有定时器:返回之前的定时器剩余时间
eg1.
alarm(10); // 返回0
过了1s...
alarm(5); // 返回上一个定时器剩余的时间 即为9
alarm(100) -> 该函数是不阻塞的,可以继续执行其他代码。
eg2.
int main()
{
int seconds = alarm(1);
for(int i = 0; ; i++) printf("%d\n", i);
return 0;
}
程序实际运行的时间 = 内核时间 + 用户时间 + 消耗的时间(IO等)
操作内存:
内核时间:程序执行系统调用的时间
(如alarm()系统函数等 包括切换到内核运行到切换回用户区继续执行同样需要消耗时间)
用户时间:程序正常代码向下运行,该进程占用CPU的使用时间
操作硬件:
IO等消耗的时间
进行文件IO操作的时候比较浪费时间
注意:定时器与进程的状态无关(采用自然定时法)。无论进程采用什么状态,alarm()都会计时。
alarm的定时时间包含的是:用户+系统内核的运行时间
alarm和setitimer(ITIMER_PROF) 共享同一个定时器
setitmer():
#include
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
功能:设置定时器(闹钟),可以替代alarm函数。精度是微秒,可以实现周期性的定时
参数:
- which: 定时器以 什么时间 计时
ITIMER_REAL: 真实时间,每次时间到达发送 SIGALRM 信号 (常用!)
ITIMER_VIRTUAL: 用户时间,每次时间到达发送 SIGVTALRM 信号
ITIMER_PROF: 以该进程在用户态 + 内核态下所消耗的时间来计算,每次时间到达发送 SIGPROF 信号
- new_value: 设置定时器属性
struct itimerval { // 定时器的结构体
struct timeval it_interval; // 每个阶段的时间,间隔时间
struct timeval it_value; // 延迟多长时间执行定时器
};
eg. 过十秒后,每隔2秒定时一次。 it_interval = 2; it_value = 10;
struct timeval { // 时间的结构体
time_t tv_sec; // seconds
suseconds_t tv_usec; // microseconds
};
- old_value: 记录上一次的定时的时间参数。一般不使用,指定 NULL
返回值:
成功:返回 0
失败:返回-1,并设置errno
示例:
#include
#include
#include
#include
// 过3秒以后每隔2秒钟定时1次
int main()
{
struct itimerval new_value;
// 设置间隔时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟时间启动计时器(延迟时间到后发送第一个信号)
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1)
{
perror("setitimer");
exit(-1);
}
getchar();
return 0;
}
#include
typedef void (*__sighandler_t)(int);
函数指针:
函数指针的名称:sighandler_t (函数的名称对应函数的地址)
返回值:void
参数:int 表示捕捉到的信号的值
__sighandler_t signal(int signum, __sighandler_t handler);
功能:设置某个信号的捕捉行为
参数:
- signum: 要捕捉的信号 (最好使用宏值)
- handler: 捕捉到信号要如何处理
- SIG_IGN: 忽略信号
- SIG_DFL: 使用信号默认行为
- 回调函数:这个函数是内核调用 程序员只负责写这个函数,捕捉到信号后如何去处理信号
回调函数:
- 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义。
- 不是程序员调用,而是当信号产生,由内核调用。
- 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置即可。
返回值:
- 成功:返回上一次注册的信号处理函数的地址handler,第一次调用返回NULL
- 失败:返回SIG_ERR,设置errno
eg.
void myalarm(int num)
{
printf("捕捉到了信号的编号是: %d\n", num);
printf("xxxxxxx\n");
}
__sighandler_t res = signal(SIGALRM, myalarm);
注意:SIGKILL SIGSTOP 不能被捕捉/忽略
#include
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
功能:检查或改变信号的处理,信号捕捉。
参数:
- signum: 需要捕捉的信号的编号或者宏值(信号的名称)
- act: 捕捉到信号之后的处理动作
- oldcat: 上一次对信号捕捉相关的设置。一般不使用,使用NULL。
返回值:
- 成功:返回0
- 失败:返回-1
struct sigaction {
// 函数指针,指向的函数就是信号捕捉到后的处理函数
void (*sa_handler)(int);
// (不常用)
void (*sa_sigaction)(int, siginfo_t *, void *);
// 临时阻塞信号集,信号捕捉函数执行过程中,临时阻塞某些信号
sigset_t sa_mask;
// 使用哪一个信号处理函数对捕捉到的信号进行处理
// 这个值可以是 0,表示使用sa_handler
// 也可以是SA_SIGINFO,表示使用sa_sigaction
int sa_flags;
// 已废弃,指定 NULL
void (*sa_restorer)(void);
};
eg.
void myalarm(int num)
{
printf("捕捉到了信号的编号是: %d\n", num);
printf("xxxxxxx\n");
}
int main()
{
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
// 清空临时阻塞信号集
sigemptyset(&act.sa_mask);
// 注册信号捕捉
int res = sigaction(SIGALRM, &act, NULL);
if(res == -1)
{
perror("sigaction");
exit(-1);
}
}
内核实现信号捕捉的过程:
注意:
用户通过键盘 Ctrl + C,产生2号信号SIGINT (信号被创建)
信号产生但是没有被处理 (未决)
这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
如果没有阻塞,这个信号就被处理
如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
以下信号集相关的函数都是对自定义的信号集进行操作:
int sigemptyset(sigset_t *set)
功能:清空信号集中的数据,将信号集中的所有标志位 置为0
参数:
- set: 传出参数,需要操作的信号集
sigset_t: 64位整数
返回值:
- 成功:返回0
- 失败:返回-1,设置errno
eg.
sigset_t set;
sigemptyset(&set);
int sigfillset(sigset_t*set);
功能:将信号集中的所有标志位 置为 1
参数:
- set: 传出参数,需要操作的信号集
返回值:
- 成功:返回0
- 失败:返回-1,设置errno
eg. sigfillset(&set);
int sigaddset(sigset_t *set, int signum);
功能:设置信号集中的某一个信号对应的标志位,将其置为1,表示阻塞该信号。
参数:
- set: 传出参数,需要操作的信号集
- signum: 需要设置阻塞的信号
返回值:
- 成功:返回0
- 失败:返回-1,设置errno
eg. sigaddset(&set, SIGINT);
int sigdelset(sigset_t *set,int signum);
功能:设置信号集中的某一个信号对应的标志位,将其置为0,表示阻塞该信号。
参数:
- set: 传出参数,需要操作的信号集
- signum: 需要设置不阻塞的信号
返回值:
- 成功:返回0
- 失败:返回-1,设置errno
eg. sigdelset(&set, SIGINT);
int sigismember(const sigset_t *set, int signum);
功能:判断某个信号是否阻塞
参数:
- set: 需要查看的信号集
- signum: 需要判断的信号
返回值:
- 1:signum被阻塞
- 0:signum不阻塞
- -1:调用失败
eg.
ret = sigismember(&set, SIGQUIT);
if(ret == 0) printf("SIGQUIT 不阻塞\n");
else if(ret == 1) printf("SIGQUIT 阻塞\n");
如果需要修改内核中的阻塞信号集或者查看未决信号集,需要使用以下函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:将自定义信号集中的数据设置到内核中(设置阻塞;解除阻塞;替换)
参数:
- how: 如何对内核阻塞信号集进行处理
SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变 (取按位或)
假设内核中默认的阻塞信号集是mask, 最后内核中的阻塞信号集 mask |= set
SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞。
假设内核中默认的阻塞信号集是mask, 最后内核中的阻塞信号集 mask &= ~set
SIG_SETMASK: 覆盖内核中原来的值
- set: 已经初始化好的用户自定义的信号集
- oldset: 保存设置之前的内核中的阻塞信号集的状态,可以是NULL。
返回值:
- 成功:返回0
- 失败:返回-1,设置errno: (EFAULT or EINVAL)
eg. sigprocmask(SIG_UNBLOCK, &set, NULL);
int sigpending(sigset_t *set);
功能:获取内核中的未决信号集
参数:
- set: 传出参数,保存的是内核中的未决信号集中的信息。
eg. sigpending(&pendingset);
案例:编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号
int main()
{
// 设置2号信号和3号信号为阻塞
sigset_t set;
sigemptyset(&set);
// 将2号和3号信号添加到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
int num = 0;
while(1)
{
// 获取当前的未决信号集中的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);
num++;
// 遍历前31位
for(int i = 1; i < 32; i++)
{
if(sigismember(&pendingset, i) == 1) printf("1");
else if(sigismember(&pendingset, i) == 0) printf("0");
else{
perror("sigismember");
exit(-1);
}
}
printf("\n");
sleep(1);
if(num == 10)
{
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}
注意:如果导入了#include
那么如下设置:
#define _BSD_SOURCE (不建议)
#define _DEFAULT_SOURCE
In file included from /usr/include/signal.h:25:0,
from sigset.c:51:
/usr/include/features.h:184:3: warning: #warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE" [-Wcpp]
# warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE"
^~~~~~~
注意:
# 加 & 号,在后台运行,在控制台可以继续进行输入指令
./main &
# 让在后台运行的进程切换回前台: fg
# 此时在控制台不可以输入指令
fg
SIGCHLD信号产生的条件
以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号
# 使用SIGCHLD信号解决僵尸进程的问题:
#define _DEFAULT_SOURCE
#include
#include
#include
#include
#include
#include
void myFun(int num)
{
printf("捕捉到的信号: %d\n", num);
// 回收子进程PCB的资源
// wait(NULL) // 这样同时有大量子进程结束时,只能回收一个进程资源,其余的都抛弃了
// while(1) wait(NULL); // 这样的话虽然可以解决上面问题,但是这样会一直停留在回调函数,无法返回主控制流程
while(1)
{
int ret = waitpid(-1, NULL, WNOHANG);
if(ret > 0) printf("child die, pid = %d\n", ret);
else if(ret == 0) break; // 说明还有子进程活着
else if(ret == -1) break; // 说明没有子进程了
}
}
int main()
{
// 提前设置好阻塞信号集,阻塞SIGCHLD
// 因为有可能子进程很快结束,父进程还没有注册完信号捕捉
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
// 创建一些子进程
__pid_t pid;
for(int i = 0; i < 20; i++)
{
pid = fork();
if(pid == 0) break;
}
if(pid > 0)
{
// 父进程
// 捕捉子进程结束时发送的SIGCHLD信号
struct sigaction act;
act.sa_handler = myFun;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// 注册完信号捕捉以后,解除阻塞。
sigprocmask(SIG_UNBLOCK, &set, NULL);
while(1) {
printf("parent process pid: %d\n", getpid());
sleep(2);
}
}
else if(pid == 0) printf("child process pid: %d\n", getpid()); // 子进程
return 0;
共享内存使用步骤:
共享内存相关的函数:
#include
#include
int shmget(key_t key, size_t size, int shmflg);
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识(ID)。
新创建的内存段中的数据都会被初始化为0
- 参数:
- key : key_t类型是一个整型,通过这个找到或者创建一个共享内存。
一般使用16进制表示,非0值
- size: 共享内存的大小 (以实际分页的大小来创建分配(PAGE_SIZE 的整数倍))
- shmflg: 属性
- 访问权限
- 附加属性:创建 或者 判断共享内存是不是存在
- 创建:IPC_CREAT
- 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
eg. IPC_CREAT | IPC_EXCL | 0664
- 返回值:
失败:-1 并设置错误号
成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
eg. int shmid = shmget(100, 4096, IPC_CREAT | 0664);
int shmid = shmget(100, 0, IPC_CREAT); // size = 0 表示获取共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 功能:和当前的进程进行关联
- 参数:
- shmid : 共享内存的标识(ID),由shmget返回值获取
- shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
- shmflg : 对共享内存的操作
- 读 : SHM_RDONLY, 且必须要有读权限
- 读写: 0
- 返回值:
成功:返回共享内存的首(起始)地址。
失败:返回 (void *) -1
eg.void* ptr = shmat(shmid, NULL, 0);
int shmdt(const void *shmaddr);
- 功能:解除当前进程和共享内存的关联
- 参数:
shmaddr:共享内存的首地址
- 返回值:成功 0, 失败 -1
eg. shmdt(ptr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能:对共享内存进行操作。(主要是删除共享内存,共享内存要删除才会消失)。
创建共享内存的进程被销毁了对共享内存是没有任何影响的。
- 参数:
- shmid: 共享内存的ID
- cmd : 要做的操作
- IPC_STAT : 获取共享内存的当前的状态
- IPC_SET : 设置共享内存的状态
- IPC_RMID: 标记共享内存被销毁
(实际上只有所有进程都对该共享内存解除关联才能执行销毁操作,现在只是标记)
- buf:需要设置或者获取的共享内存的属性信息
- IPC_STAT : buf用来存储数据(传出参数)
- IPC_SET : buf中需要初始化数据,设置到内核中
- IPC_RMID : 没有用,NULL
eg. shmctl(shmid, IPC_RMID, NULL); // 删除共享内存
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
key_t ftok(const char *pathname, int proj_id);
- 功能:根据指定的路径名,和int值,生成一个共享内存的key
- 参数:
- pathname:指定一个存在的路径
/home/boyangcao/Linux/a.txt
/
- proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
范围 : 0-255 一般指定一个字符 'a'
共享内存操作命令
ipcs用法:
ipcs -a # 打印当前系统中所有的进程间通信方式的信息
ipcs -m # 打印出使用共享内存进行进程间通信的信息
ipcs -q # 打印出使用消息队列进行进程间通信的信息
ipcs -s # 打印出使用信号进行进程间通信的信息
ipcrm用法:
ipcrm -M shmkey # 移除用shmkey创建的共享内存段
ipcrm -m shmid # 移除用shmid标识的共享内存段
ipcrm -Q msgkey # 移除用msqkey创建的消息队列
ipcrm -q msqid # 移除用msqid标识的消息队列
ipcrm -s semkey # 移除用semkey创建的信号
ipcrm -s semid # 移除用semid标识的信号
问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
shm_nattch 记录了关联的进程个数
问题2:可不可以对共享内存进行多次删除 shmctl
可以的
因为shmctl 标记删除共享内存,不是直接删除
什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除
当共享内存的key为0(0x00000000)的时候,表示共享内存被标记删除了
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能再次进行关联。
共享内存和内存映射的区别
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高
3.内存
所有的进程操作的是同一块共享内存。
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全
- 进程突然退出
共享内存还存在
内存映射区消失
- 运行进程的电脑死机,宕机了
数据存储在共享内存中,就没有了
内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
5.生命周期
- 内存映射区:进程退出,内存映射区销毁
- 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。