系统编程 学习笔记 02

回收子进程

孤儿进程

父进程先于子进程结束,子进程的父进程成为 init 进程。

僵尸进程

子进程终止,父进程尚未回收,子进程残留的资源 (PCB) 存放于内核中,变成僵尸 (Zoombie) 进程。

wait 函数

一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪一个。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息, 然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在 shell 中用特殊变量 $? 查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程

  • 父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:
    • 阻塞等待子进程退出

    • 回收子进程残留资源

    • 获取子进程结束状态(推出原因)

    • pid_t wait(int *status); 成功:清除掉的子进程ID; 失败:-1(没有子进程)

当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2.释放用户空间分配的内存。内存的 PCB 仍然存在。其中保存该进程的退出状态(正常终止->退出值;异常终止->终止信号)

**可使用 wait 函数传出的参数 status 来保存进程的退出状态。**借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:

  • WIFEXITED(status) 为非 0 -> 进程正常结束

    • WEXITSTATUS(status) 如上宏为真,使用此宏 -> 获取进程退出状态( exit 的参数)
  • WIFSIGNALED(status) 为非 0 -> 进程异常终止

    • WTERMSIG(status) 如上宏为真,使用此宏 -> 取得使进程终止的哪个信号的编号
  • WIFSTOPPED(status) 为非 0 -> 进程处于暂停状态

    • WSTOPSIG(status) 如上宏为真,使用此宏 -> 取得使进程暂停得那个信号得编号。
    • WIFCONTINUED(status) 为真 -> 进程暂停后已经继续运行

waitpid 函数

作用同 wait, 但可指定 pid 进程清理,可以不阻塞。

  • pid_t waitpid(pid_t pid, int *status, int options);

    • 成功:返回清理掉得子进程 ID;
    • 失败:-1(无子进程)
  • 特殊参数和返回情况:

  • 参数 pid:

    • pid>0: 回收指定 ID 得子进程

    • pid==0: 回收和当前调用 waitpid 一个组任意子进程

    • pid==-1: 回收任意子进程 (相当于 wait)

    • pid<-1: 回收指定进程组内的任意子进程

  • 参数 options:

    • 为 WNOHANG,不阻塞,返回 0,代表子进程正在进行。
    • 为 0,阻塞。

注意:一次 wait 或 waitpid 调 用只能清理一个子进程,清理多个子进程应使用循环。

练习:父进程 fork 3 个子进程,三个子进程一个调用 ps 命令,一个调用自定义程序 1(正常),一个调用自定义程序 2(会段错误)。父进程使用 waitpid 对其子进程进行回收。

#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
int main(void) {
    int i;
    for(i = 1; i <= 3; i++) {
        pid_t pid = fork();
        if(pid == -1) {
            perror("fork error");
            exit(1);
        }
        if(pid == 0) {
            if(i == 1) {
                execlp("ps", "ps", "aux", NULL); 
            } else if(i == 2) {
                execl("./normal", "normal", NULL);//当前目录下写个正常程序
            } else {
                execl("./abnormal", "abnormal", NULL);//当前目录下写个段错误程序
            }
            break;
        }
    }
    if(i > 3) {
        int status;
        while(1) {
            int t = waitpid(-1, &status, WNOHANG);
            if(t == -1) break;
            if(t == 0) {
                sleep(1);
                continue;
            }
            if(WIFEXITED(status)) {
                printf("normal %d %d\n", t, WEXITSTATUS(status));
            }
            else if(WIFSIGNALED(status)) {
                printf("abnormal %d %d\n", t, WTERMSIG(status));
            }
        }
    }
    return 0;

}

IPC方法(interProcess Communication)

ulimit 是一个计算机命令,用于shell启动进程所占用的资源,参数形式有-H设置硬资源限制;-S 设置软资源限制;-a 显示当前所有的资源限制等。

Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

在进程间完成数据传递需要借助操作系统提供特殊的方法,如:**文件、管道、信号、共享内存、消息队列、套接字、命名管道等。**随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方法有:

  • 管道(使用最简单)

    • pipe
    • fifo: (有名管道)
      • 用于非血缘关系进程间通信
  • 信号(开销最小)

  • 共享映射区(无血缘关系)

  • 本地套接字(最稳定)

管道的概念:

管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一个管道。有如下特质:

  • 其本质是一个伪文件(实为内核缓冲区) // 伪文件不占用磁盘存储 s, b, c, p

  • 由两个文件描述符引用,一个表示读端,一个表示写端

  • 规定数据从管道的写端流入管道,从读端流出。

管道的原理:管道实为内核使用环形队列机制,借助**内核缓冲区(4k)**实现。

管道的局限性:

  • 数据自己读不能自己写。

  • 数据一旦被读走,便不再管道中存在,不可反复读取。

  • 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。

  • 只能在由公共祖先的进程间使用管道。

常用的通信方式有:单工通信、半双工通信、全双工通信。

pipe 函数

创建管道

  • int pipe(int pipefd[2]);

    • 成功: 0;
    • 失败: -1; 设置 errno
  • 函数调用成功返回 r/w 两个文件描述符。无需 open, 但需手动 close。

  • 规定:fd[0] -> r; fd[1] -> w。向管道文件读写数据其实是在读写内核缓冲区。

练习:利用 pipe 函数,子进程向父进程发送信息。

#include <cstdio>
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
int main(void)
{
    int fd[2];
    int ret = pipe(fd);
    if(ret == -1) {
        perror("pipe error");
        exit(1);
    }
    pid_t pid = fork();
    if(pid == -1) {
        perror("pid error");
        exit(1);
    } else if(pid == 0) {
        close(fd[0]); //close r
        write(fd[1], "parent hello\n", strlen("parent hellor\n"));
    } else {
        sleep(1);
        close(fd[1]);//close w
        char buff[2048];
        int size = read(fd[0], buff, sizeof(buff));
        write(STDOUT_FILENO, buff, size);
    }
    return 0;
}

管道读写行为

  • 读管道
    • 管道中有数据
      • read 返回实际读到的字节数
    • 管道中无数据
      • 写端全关闭:read 返回 0
      • 仍有写端打开:阻塞等待
  • 写管道
    • 读端全关闭
      • 进程异常终止(SIGPIPE 信号)
    • 有读端打开
      • 管道未满:写数据,返回写入字节数
      • 管道已满:阻塞(少见)

管道优劣

  • 优点:实现手段简单

  • 缺点

    • 单向通信
    • 只能有血缘关系进程间使用

FIFO

  • 命名管道(Linux 基础文件类型)
  • mkfifo 函数
    • 参数:
      • name
      • mode: 8进制
    • 返回值:
      • 成功:0
      • 失败:-1,设置 errno
  • 无血缘关系进程间通信
    • 使用同一 FIFO
    • 可多读端,多写端。

共享存储映射

文件进程间通信

使用文件也可以完成 IPC,理论依据是,fork 后,父子进程共享文件描述符。也就共享打开的文件。

练习:编程测试,父子进程共享打开的文件。借助文件进行进程间通信。

#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;
void sys_error(const char *s)
{
    perror(s);
    exit(1);
}
int main(void)
{
    pid_t pid;
    pid = fork();
    int fd;
    if(pid == -1) {
        sys_error("fork error");
    }
    else if(pid == 0) {
        fd = open("file_IPC", O_RDWR|O_CREAT, 0644);
        if(fd == -1) {
            sys_error("open error");
        }
        write(fd, "hello father\n", strlen("hello father\n"));
        sleep(2);
        char buff[2048];
        int size = read(fd, buff, sizeof(buff));
        write(STDOUT_FILENO, buff, size);
    }
    else {
        sleep(1);
        fd = open("file_IPC", O_RDWR);
        if(fd == -1) {
            sys_error("open error");
        }
        char buff[2048];
        int size = read(fd, buff, sizeof(buff));
        write(STDOUT_FILENO, buff, size);
        write(fd, "hello son\n", strlen("hello son\n"));
        sleep(1);
    }
    close(fd);
     
    return 0;
}

存储映射 I/O

存储映射 I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可以在不实用 read 和 write 函数的情况下,使用地址(指针)完成 I/O 操作。

使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过 mmap 函数来实现。

mmap 函数

void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);

  • 返回:

    • 成功:返回创建的映射区的首地址
    • 失败:MAP_FAILED
  • addr: 建立映射区的首地址,由 linux 内核指定。使用时,直接传递NULL

  • length: 欲创建映射区的大小

  • prot: 映射器权限 PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE

  • flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)

    • MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上。
    • MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
  • fd: 用来建立映射区的文件描述符

  • offset: 映射文件的偏移(4k 的整数倍)

munmap 函数

int munmap(void *addr, size_t length);

  • 返回:
    • 成功: 0;
    • 失败:-1;
  • 参数:
    • mmap 返回值
    • 映射区大小

mmap 注意事项

  • 创建映射区的过程中,隐含着一次对映射文件的读操作。
  • 当 MAP_SHARED 时,要求:映射区的权限 <= 文件打开权限(出于对映射区的保护)。而 MAP_PRIVATE 则无所谓,因为 mmap 中的权限是对内存的限制。
  • 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭
  • 特别注意,当映射文件大小为 0 时,不能创建映射区。所以:用于映射的文件必须要有实际大小!! mmap 使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
  • munmap 传入的地址一定是 mmap 的返回地址。坚决杜绝指针++操作。
  • 如果文件偏移量必须为 4K 的整数倍(mmu的最小操作单位)
  • mmap 创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

mmap 父子进程通信

父子等有血缘关系的进程之间也可以通过 mmap 建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数 flags:

  • MAP_PRIVATE: (私有映射) 父子进程各自独占映射区

  • MAP_SHARED: (共享映射) 父子进程共享映射区

练习:父进程创建映射区,然后 fork 子进程,子进程修改映射区内容,而后,父进程读取映射区内容,查验是否共享。

#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
using namespace std;
int var = 100;
int main(void)
{
    int fd = open("./fmmap_ps", O_CREAT|O_RDWR|O_TRUNC, 0644);
    if(fd < 0) {
        perror("open error");
        exit(1);
    }
    unlink("./fmmap_ps");//删除临时文件目录项,当所有占用该文件的进程都结束后,文件就被删除。
    ftruncate(fd, 4);

    int *p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED) {
        perror("mmap error");
        exit(1);
    }
    close(fd);
    pid_t pid = fork();
    if(pid == -1) {
        perror("fork error");
        exit(1);
    }
    else if(pid == 0) {
        *p = 5;
        var = 5;
        printf("p = %d, var = %d\n", *p, var);
    }
    else {
        sleep(1);
        printf("p = %d, var = %d\n", *p, var);
    }
    munmap(p, 4);
    return 0;
}

结论:父子进程共享:1. 打开的文件 2. mmap 建立的映射区(但必须要使用 MAP_SHARED)

匿名映射

通过使用发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要 open 一个 temp 文件,创建好了再 unlink、close 掉,比较麻烦。可以直接使用匿名映射来代替。其实 Linux 系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标识位参数 flags 来指定。

使用 MAP_ANONYMOUS(或 MAP_ANON), 如:

  • int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

  • “4” 随意举例,该位置表大小,可依据实际需要填写。

需注意的是,MAP_ANONYMOUS 和 MAP_ANON 这两个宏是 linux 操作系统特有的宏。

在类 Unix 系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。

  • (1) fd = open("/dev/zero", O_RDWR);// 可以把所有的东西都扔进 /dev/null

  • (2) p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

strace 是个功能强大的Linux调试分析诊断工具,可用于跟踪程序执行时进程系统调用(system call)和所接收的信号,尤其是针对源码不可读或源码无法再编译的程序

  • 用法:strace 可执行文件

无血缘关系进程间 mmap 通信

  • 使用同一文件创建映射区
  • 指定 MAP_SHARED
  • 可多读端,多写端
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值