Linux多进程开发(4):内存映射、信号

内存映射

  • 内存映射(Memory-mapped I/O) 是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。

01 内存映射相关系统调用

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
/*
功能:将一个文件或者设备的数据映射到内存中。指令 man 2 mmap 查看详情
头文件:#include <sys/mman.h>
参数:
    void * addr:要映射到的内存地址,一般填NULL,表示由内核指定
    length:要映射的数据的长度,这个值不能为0。建议使用文件的长度。
        获取文件的长度: stat、lseek
    prot:对申请的内存映射区的操作权限
        PROT_EXEC:可执行的权限
        PROT_READ:读权限
        PROT_WRITE:写权限
        PROT_NONE:没有权限
        要操作映射内存,必须要有读的权限,可以设置成PROT_READ、PROT_READ | PROT_WRITE,一般是后者
    flags:
        MAP_SHARED :映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
        MAP_PRIVATE:不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
    fd:需要映射的那个文件的文件描述符
        通过open得到,open的是一个磁盘文件
        注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
            prot设置为:PROT_ READ                open可以设置为:只读/读写
            prot设置为:PROT_ READ| PROT WRITE    open可以设置为:读写
        也就是说prot的权限不能大于open的权限
    offset:偏移量。一般不用,必须指定的是4k的整数倍。一般用0,表示不偏移。
返回值:返回创建的内存的首地址
        失败返回MAP_FAILED,是一个宏,实质是(void *)-1
*/

int munmap(void *addr, size_t length);
功能:释放内存映射,指令 man 2 munmap 查看详情
头文件:#include <sys/mman.h>
参数:
    addr:要释放的内存的首地址,mmap函数得到
    length:要释放的内存的大小,要和mmap函数中的length参数的值一样。
返回值:
    成功返回 0
    失败返回-1

使用内存映射实现进程间通信:

  1. 有关系的进程(父子进程)
    在还没有子进程的时候,通过唯一的父进程,先创建内存映射区,有了内存映射区以后,创建子进程,父子进程共享创建的内存映射区
  2. 没有关系的进程间通信
    准备一个大小不是0的磁盘文件,进程1通过磁盘文件创建内存映射区,得到一个操作这块内存的指针。然后进程2也通过磁盘文件创建内存映射区,得到一个操作这块内存的指针,使用内存映射区通信

注意:内存映射区通信,是非阻塞。

02 思考问题

  1. 如果对mmap的返回值(ptr)做++操作(ptr++),munmap是否能够成功?
    可以对其进行++操作,但是再用此时的ptr调用munmap会出错,如果想++还要munmap,那需要把原始的ptr拷贝一份,调用munmap时用拷贝的ptr。

  2. 如果open时O_RDONLY,mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
    会出现错误,并返回MAP_FAILED。open()函数中的权限建议和prot参数的权限保持致。

  3. 如果文件偏移量为1000会怎样?
    会出错并返回MAP_FAILED,文件偏移量必须为4k(4096)的整数倍。

  4. mmap什么情况下会调用失败?
    第二个参数length = 0;第三个参数prot的权限没有读的权限或比fd的flags权限大;文件偏移量不是4k的整数倍

  5. 可以open的时候O_CREAT一个新文件来创建映射区吗?
    可以,但是新创建的文件大小不能为0,可以通过lseek函数、truncate函数扩展大小

  6. mmap后关闭文件描述符,对mmap映射有没有影响?
    没有影响,因为mmap函数内部实际上对传入的文件描述符做了拷贝

  7. 对ptr越界操作会怎样?
    越界操作所操作的是非法的内存,会导致段错误

03 mmap函数可以拷贝文件

原理:假设复制test.txt为copy.txt,首先将test.txt创建内存映射区得到指针ptr1,然后创建copy.txt文件,然后新建copy.txt文件并将其大小扩展为与test.txt大小相同,记得写入一个空格,否则扩展后的大小无效,将copy.txt创建内存映射区得到指针ptr2,将ptr1拷贝到ptr2处,释放资源即可。
注意:释放资源时先打开的后释放,因为可能由于依赖关系会造成某些错误。mmap一般不用来拷贝文件,但是确实有这种功能,mmap不能拷贝大文件,因为文件内容是要写入内存的。

04 匿名内存映射

匿名内存映射可以不创建实际的文件从而进行进程间通信,但是只能用于有亲缘关系的进程之间的通信,原因与匿名管道只能用于有亲缘关系的进程之间的通信类似。
用法:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
flags要设置为MAP_SHARED | MAP_ANONYMOUS,在设置了MAP_ANONYMOUS后,fd与offset会被忽略,有时fd会被要求为-1,所以fd设置为-1,offset设置为0即可

信号

01 信号的概念

  • 信号是Linux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

  • 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
    1.对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号。
    2.硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被0除,或者引用了无法访问的内存区域。
    3.系统状态变化,比如alarm定时器到期将引起SIGALRM信号,进程执行的CPU时间超限,或者该进程的某个子进程退出。
    4.运行kill命令或调用kill函数。

  • 使用信号的两个主要目的是:
    1.让进程知道已经发生了一个特定的事情。
    2.强迫进程执行它自己代码中的信号处理程序。

  • 信号的特点:
    1.简单
    2.不能携带大量信息
    3.满足某个特定条件才发送
    4.优先级比较高

  • 查看系统定义的信号列表: kill -l

  • 前31个信号为常规信号,其余为实时信号。

02 Linux信号一览表

加粗的信号要求掌握

编号 信号名称 对应事件 默认动作
1 SIGHUP 用户退出shell时,由该shell启动的所有进程将收到这个信号 终止进程
2 SIGINT 当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 终止进程
3 SIGQUIT 用户按下<Ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 终止进程
4 SIGILL CPU检测到某进程执行了非法指令 终止进程并产生core文件
5 SIGTRAP 该信号由断点指令或其他trap指令产生 终止进程并产生core文件
6 SIGABRT 调用abort函数时产生该信号 终止进程并产生core文件
7 SIGBUS 非法访问内存地址,包括内存对齐出错 终止进程并产生core文件
8 SIGFPE 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 终止进程并产生core文件
9 SIGKILL 无条件终止进程。该信号不能被忽略,处理和阻塞 终止进程,可以杀死任何进程
10 SIGUSE1 用户定义的信号。即程序员可以在程序中定义并使用该信号 终止进程
11 SIGSEGV 指示进程进行了无效内存访问(段错误) 终止进程并产生core文件
12 SIGUSR2 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 终止进程
13 SIGPIPE Broken pipe向 一个没有读端的管道写数据 终止进程
14 SIGALRM 定时器超时,超时的时间由系统调用alarm设置 终止进程
15 SIGTERM 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号 终止进程
16 SIGSTKFLT Linux早期版本出现的信号,现仍保留向后兼容 终止进程
17 SIGCHLD 子进程结束时,父进程会收到这个信号 忽略这个信号
18 SIGCONT 如果进程已停止,则使其继续运行 继续/忽略
19 SIGSTOP 停止进程的执行。信号不能被忽略,处理和阻塞 终止进程
20 SIGTSTP 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号 暂停进程
21 SIGTTIN 后台进程读终端控制台 暂停进程
22 SIGTTOU 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生 暂停进程
23 SIGURG 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 忽略该信号
24 SIGXCPU 进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程 终止进程
25 SIGXFSZ 超过文件的最大长度设置 终止进程
26 SIGVTALRM 虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间 终止进程
27 SIGPROF 类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间 终止进程
28 SIGWINCH 窗口变化大小时发出 忽略该信号
29 SIGIO 此信号向进程指示发出了一个异步IO事件 忽略该信号
30 SIGPWR 关机 终止进程
31 SIGSYS 无效的系统调用 终止进程并产生core文件
34~64 SIGRTMIN~SIGRTMAX LINUX的实时信号,它们没有固定的含义(可以由用户自定义) 终止进程

03 信号的5种默认处理动作

  • 查看信号的详细信息: man 7 signal。会遇到一个信号有多个值的情况,这是因为不同架构的信号的值可能不同,linux系统看中间的值就可以了
  • 信号的5中默认处理动作
Term 终止进程
Ign 当前进程忽略掉这个信号
Core 终止进程,并生成一个Core文件
Stop 暂停当前进程
Cont 继续执行当前被暂停的进程
  • 信号的几种状态:产生(事件发生产生信号)、未决(信号未处理)、递达(信号已处理)
  • SIGKILL和SIGSTOP信号不能被捕捉、阻塞或者忽略,只能执行默认动作。

生成core文件

ulimit -a指令可以看到core文件的大小是0,所以此时还不会产生core文件,可以通过ulimit -c 文件大小指令修改core文件的大小,之后在编译的时候加上参数-g,在GDB中利用指令core-file core文件名查看core文件

04 信号相关的函数

int kill(pid_t pid, int sig);
/*
功能:给任何的进程或者进程组pid,发送任何的信号sig。指令 man 3 kill 查看详情
头文件:#include <signal.h>
参数:
    pid:
        >0:将信号发送给指定的进程
        =0:将信号发送给当前的进程组
        =-1:将信号发送给每一个有权限接收这个信号的进程
        <-1:将信号发送 进程组ID等于pid的绝对值 的进程组的所有进程
    sig:需要发送的信号的编号或者是宏值,0表示不发送任何信号。推荐使用宏
返回值:
    成功返回 0
    失败返回-1
示例:给父进程发送9号信号,kill(getppid(), 9);
*/


int raise(int sig);
/*
功能:给当前进程发送信号。指令 man 3 raise 查看详情
头文件:#include <signal.h>
参数:
    sig:要发送的信号
返回值:
    成功返回 0
    失败返回 非0
raise(sig)相当于:kill(getpid(), sig);
*/


void abort(void);
/*
功能:送SIGABRT信号给当前的进程,杀死当前进程。指令 man 3 abort 查看详情
头文件:#include <stdlib.h>
相当于:kill(getpid(), SIGABRT); 
*/


unsigned int alarm(unsigned int seconds);
/*
功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM。指令 man 3 alarm 查看详情。
头文件:#include <unistd.h>
参数:
    seconds:倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时)。取消一个定时器,通过alarm(0)。
返回值:
    之前没有定时器,返回0
    之前有定时器,返回之前的定时器剩余的时间
SIGALARM:默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。后面调用的alarm会覆盖前面的
例:当第一次调用alarm(10),程序会10秒之后终止,由于之前没有调用alarm,所以返回值是0。过了1秒,再次调用alarm(5),此时会覆盖之前的alarm(10),不是9秒后程序终止,而是5秒后,此时返回值是10 - 1 = 9。
注意:
    alarm函数是非阻塞的。
    程序运行实际的时间 = 内核时间 + 用户时间 + 消耗的时间。进行文件IO操作的时候比较浪费时间。
    定时器与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/


int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);
/*
功能:设置定时器(闹钟)。可以替代alarm函数。精度微秒us,可以实现周期性定时。指令 man 3 setitimer 查看详情
头文件:#include <sys/time.h>
参数:
    which:定时器以什么时间计时
        ITIMER_REAL:真实时间,时间到达,发送SIGALRM。(常用)
        ITIMER_VIRTUAL:用户时间,时间到达,发送SIGVTALRM。
        ITIMER_PROF:以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送SIGPROF。
    new_value:设置定时器的属性
        struct itimerval 
        {    // 定时器的结构体
            struct timeval it_interval;     //间隔时间
            struct timeval it_value;        //延迟多长时间执行定时器
        };
        struct timeval 
        {
            //时间的结构体
            time_t tv_sec;          //秒
            suseconds_t tv_usec;    //微秒
        };
    old_value:记录上一次的定时的时间参数。一般不使用,指定NULL
返回值:
    成功返回 0
    失败返回-1

setitimer工作机制是,先对it_value倒计时,当it_value为零时触发信号。然后将it_value重置为it_interval。继续对it_value倒计时。一直这样循环下去。
基于此机制。setitimer既能够用来延时运行,也可定时运行。
假如it_value为0是不会触发信号的,所以要能触发信号,it_value得大于0;假设it_interval为零,在it_value倒计时为0时仅会触发一次信号,而不会周期性触发
setitimer是非阻塞的
*/

05 信号捕捉函数

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
/*
功能:设置某个信号的捕捉行为
头文件:#include <signal.h>
参数:
    signum:要捕捉的信号
    handler:捕捉到信号要如何处理
        SIG_IGN:忽略信号
        SIG_DFL:使用信号默认的行为
        回调函数:程序员负责实现,由内核调用,捕捉到信号后执行回调函数
回调函数:
    需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
    不是程序员调用,而是当信号产生,由内核调用
    函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置即可
返回值:
    成功,返回上一次注册的信号处理函数(回调函数)的地址。第一次调用返回NULL
    失败,返回SIG_ERR,设置错误号
注意:要在定时之前注册信号捕捉,否则信号已经发出才注册,有可能捕捉不到
*/

typedef void (*sighandler_t)(int);    //函数指针,int类型的参数表示捕捉到的信号的值。

//回调函数示例:
void myalarm(int SIGALRM)
{
    //......捕捉到信号后的动作
}

//信号捕捉示例
signal(int SIGALRM, myalarm);    //回调函数填入函数名即可

//信号捕捉函数sigaction,由于这个函数涉及信号集知识,所以后面再介绍
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

注意事项:

  1. 尽量避免使用signal函数,采用sigaction函数去捕捉信号。因为适用的标准不同,sigaction比signal适用性更广泛。
  2. sigaction在捕捉信号的时候,会使用第二个参数中临时的阻塞信号集sa_mask,函数执行完以后会恢复使用系统的阻塞信号集。
  3. 在执行某个回调函数期间,再次发送 与回调函数捕捉的信号 相同的信号,默认会被屏蔽(阻塞),也就是说回调函数执行期间不会再次去执行,等执行完毕后,如果再次捕捉到此信号才会再次执行回调函数。
  4. 未决信号集和阻塞信号集中常规信号不支持排队,因为这两个信号集中只有0和1的位信息,不能记录某个位有多少个信号,也就是说假设一个信号被阻塞,这个信号如果发送多次,那么这个发送多次的次数是不能被记录的,未决信号集中只会有代表该信号的二进制位被置为1。常规信号之外的信号支持排队。

06 信号集

  • 许多信号相关的系统调用都需要能表示一-组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为sigset_t。
  • 在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。(未决信号集不能修改,只能读)
  • 信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
  • 信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
  • 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

07 阻塞信号集和未决信号集

  1. 假设用户通过键盘Ctrl + C,产生2号信号SIGINT(信号被创建)
  2. 信号产生但是没有被处理(未决),在内核中将所有的没有被处理的信号存储在一个集合中(未决信号集),SIGINT信号状态被存储在第二个标志位上
    这个标志位的值为0,说明信号不是未决状态
    这个标志位的值为1,说明信号处于未决状态
  3. 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
    阻塞信号集默认不阻塞任何的信号
    如果想要阻塞某些信号需要用户调用系统的API
  4. 在处理的时候需要对阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
    如果没有阻塞,这个信号就被处理
    如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

08 信号集相关的函数

//以下五个函数都是对自定义信号集进行操作,可以通过指令 man 3 sigsetops (或 man 3 函数名)查看详情
//头文件均为#include <signal.h>

int sigemptyset(sigset_t *set);
/*
功能:清空信号集中的数据,将信号集中的所有的标志位置为0。
参数:
    set:传出参数,需要操作的信号集
返回值:
    成功返回 0
    失败返回-1
*/


int sigfillset(sigset_t *set);
/*
功能:将信号集中的所有的标志位置为1
参数:
    set:传出参数,需要操作的信号集
返回值:
    成功返回 0
    失败返回-1
*/


int sigaddset(sigset_t *set, int signum);
/*
功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
参数:
    set:传出参数,需要操作的信号集
    signum:需要阻塞的信号
返回值:
    成功返回 0
    失败返回-1
*/


int sigdelset(sigset_t *set, int signum);
/*
功能:设置信号集中的某一个信号对应的标志位为1,表示不阻塞这个信号
参数:
    set:传出参数,需要操作的信号集
    signum:需要设置不阻塞的信号
返回值:
    成功返回 0
    失败返回-1
*/


int sigismember(const sigset_t *set, int signum);
/*
功能:查询某个信号是否被设置为阻塞
参数:
    set:需要操作的信号集
    signum:需要判断的那个信号
返回值:
    1:signum被阻塞
    0:signum不阻塞
    -1:调用失败
*/
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/*
功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换),指令man 3 sigprocmask 查看详情
头文件:#include <signal.h>
参数:
    how:如何对内核阻塞信号集进行处理
        SIG_BLOCK:将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
            假设内核中默认的阻塞信号集是mask,mask |= set
        SIG_UNBLOCK:根据用户设置的数据,对内核中的数据进行解除阻塞,只解除set里面二进制位为1的信号,其他不变
            假设内核中默认的阻塞信号集是mask,mask &= ~set
        SIG_SETMASK:覆盖内核中原来的值
    set:已经初始化好的用户自定义的信号集
    oldset:保存设置之前的内核中的阻塞信号集的状态,可以是NULL
返回值:
    成功: 0
    失败:-1,设置错误号。设置错误号: EFAULT、EINVAL
*/


int sigpending(sigset_ _t *set);
/*
功能:获取内核中的未决信号集。指令 man 3 sigpending 查看详情
头文件:#include <signal.h>
参数:
    set:传出参数,保存的是内核中的未决信号集中的信息。
返回值:
    成功返回 0
    失败返回-1
*/

09 信号捕捉函数sigaction

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
/*
功能:检查或者改变信号的处理。信号捕捉。指令 man 2 sigaction 查看详情
参数:
    signum:需要捕捉的信号的编号或者宏值(信号的名称)
    act:捕捉信号之后的处理动作
    oldact:上一次对信号捕捉相关的设置,一般不使用,传递NULL
返回值:
    成功 0
    失败-1

struct sigaction {
   void     (*sa_handler)(int);     //函数指针,指向的函数就是信号捕捉到之后的处理函数,int表示捕捉的信号
   void     (*sa_sigaction)(int, siginfo_t *, void *);    //功能与上面相同,不常用
   sigset_t   sa_mask;        //临时信号阻塞集,在信号处理函数执行过程中,临时阻塞某些信号。
   int        sa_flags;      //使用哪一个信号处理对捕捉到的信号进行处理,与open的flags函数类似,多种选择用 | 隔开。这个值可以是0,表示使用sa_handler处理信号,也可以是SA_SIGINFO表示使用sa_sigaction处理信号
   void     (*sa_restorer)(void);      //已废弃,设置为NULL即可
};

*/

注意事项:

  1. 尽量避免使用signal函数,采用sigaction函数去捕捉信号。因为适用的标准不同,sigaction比signal适用性更广泛。

  2. 当sigaction捕捉到信号signum后,进入信号处理函数,假设此时有信号a发出(信号a在临时信号阻塞集sa_mask中且signum不等于信号a),信号a会被忽略并进入阻塞状态(未决),当当前信号处理函数执行完毕后,未决的信号a才会被递达,信号a的处理函数才被调用。此外,当signum的信号处理函数在执行期间,如果再次发出signum信号,此时signum也会被阻塞(即使sa_mask内没有设置任何信号),除非sa_flags中选择了SA_NODEFER选项

  3. 在执行某个回调函数期间,再次发送 与回调函数捕捉的信号 相同的信号,默认会被屏蔽(阻塞),也就是说回调函数执行期间不会再次去执行,等执行完毕后,如果再次捕捉到此信号才会再次执行回调函数。

  4. 未决信号集和阻塞信号集中常规信号不支持排队,因为这两个信号集中只有0和1的位信息,不能记录某个位有多少个信号,也就是说假设一个信号被阻塞,这个信号如果发送多次,那么这个发送多次的次数是不能被记录的,未决信号集中只会有代表该信号的二进制位被置为1。常规信号之外的信号支持排队。

10 内核实现信号捕捉的过程

11 SIGCHLD信号

  • SIGCHLD信号产生的条件
    1.子进程终止时
    2.子进程接收到SIGSTOP信号停止时
    3.子进程处在停止态,接受到SIGCONT后唤醒时
  • 以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号
posted @ 2022-09-16 21:07  小肉包i  阅读(143)  评论(0)    收藏  举报