Linux多进程开发(5):共享内存、守护进程

共享内存

01 共享内存

  • 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种IPC机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
  • 与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC 技术的速度更快。

02 共享内存使用步骤

  • 调用shmget( )创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。使用shmat( )来附上共享内存段,使该段成为调用进程的虚拟内存的一部分。
  • 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat( )调用返回的addr值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  • 调用shmdt( )来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
  • 调用shmctl( )来删除共享内存段。shmctl( )函数会给共享内存段一个标识,表示要销毁,当所有附加内存段的进程都与之分离之后内存段才会开始销毁。只有一个进程需要执行这一步。

03 内存共享操作函数

int shmget(key_t key, size_t size, int shmflg);
/*
功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。新创建的内存段中的数据都会被初始化为0。指令 man 2 shmget 查看详情。
头文件:#include <sys/ipc.h>
       #include <sys/shm.h>
参数:
    key: key_t类型是一个整形,通过这个找到或者创建一个共享内存。一般使用16进制表示,非0值
    size:共享内存的大小,必须是内存分页的整数倍
    shmflg:属性
        1.访问权限
        2.附加属性:创建共享内存或判断共享内存是否存在
            创建:IPC_CREAT
            判断共享内存是否存在:IPC_EXCL。需要和IPC_CREAT一起使用,如果已存在会创建失败
         例:IPC_CREAT| IPC_EXCL| 0664
返回值:
    失败:-1 并设置错误号
    成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
*/


void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
功能:将创建的共享内存段和当前的进程进行关联。指令 man 2 shmat 查看详情。
头文件:#include <sys/types.h>
       #include <sys/shm.h>
参数:
    shmid:共享内存的标识(ID),由shmget返回值获取
    shmaddr:申请的共享内存的起始地址,一般传入NULL,由内核指定地址
    shmflg:对共享内存的操作
        读:SHM_RDONLY,必须要有读权限
        读写:0
返回值:
    成功:返回共享内存的首(起始)地址。
    失败:(void *) -1
*/


int shmdt(const void *shmaddr);
/*
功能:解除当前进程和共亭内有的关联。指令 man 2 shmdt 查看详情。     
头文件:#include <sys/types.h>
       #include <sys/shm.h>
参数:
    shmaddr:共享内存的首地址
返回值:
    成功 0
    失败-1
*/


int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
功能:对共享内存进行操作,主要用来删除共享内存。共享内存要删除才会消失,创建共享内存的进程被销毁了对共享内存是没有任何影响的。指令 man 2 shmctl 查看详情。
头文件:#include <sys/ipc.h>
       #include <sys/shm.h>
参数:
    shmid:共享内存的ID
    cmd:要做的操作
        IPC_STAT:获取共享内存的当前的状态
        IPC_SET:设置共享内存的状态
        IPC_RMID:标记共享内存被销毁
    buf:shmid_ds类型的结构体,设置或者获取的共享内存的属性信息,结构体详情输入指令 man 2 shmctl 查看。根据cmd参数的不同,buf的作用也不同:
        IPC_STAT:buf存储数据,传出参数
        IPC_SET:buf中需要初始化数据,设置到内核中
        IPC_RMID:没有用,传入NULL
返回值:
    成功 0
    失败-1
*/


key_t ftok(const char *pathname, int proj_id);
/*
功能:根据指定的路径名和int值,生成一个共享内存的key。指令 man 3 ftok 查看详情。
头文件:#include <sys/types.h>
       #include <sys/ipc.h>
参数:
    pathname:指定一个存在的路径,该路径可以是任意的,与权限无关
    proj_id:它是一个8bit的非0整数,范围是1~255。它是自己约定的,可以是范围内任意的数。
返回值:
    成功:返回生成的key
    失败:返回-1

注意事项:
1.生成的key值可用于:msgget函数、semget函数、shmget函数
2.在指定pathname与proj_id不改变的情况下,生成的key是固定的,但是一旦指定的路径文件被删除再重新创建,生成的key就会改变。所以要确保key值不变,要么确保指定路径的文件不被删除,要么不用ftok(),而指定一个固定的key值。
*/

04 1共享内存操作命令

  • 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标识的信号

05 一些问题

  • 问题1:操作系统如何知道一块 共享内存被多少个进程关联?
    共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员shm_nattch,shm_nattach记录了关联的进程个数
  • 问题2:可不可以对共享内存进行多次删除shmctl
    可以的,因为shmctl标记删除共享内存,不是直接删除。什么时候真正删除呢?当共享内存被shmctl标记删除,并且和共享内存关联的进程数为0的时候,就真正被删除。当共享内存的key为0的时候便是共享内存被标记删除了,如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。
  • 问题3:共享内存和内存映射的区别
    1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
    2.共享内存效果更高
    3.内存不同。内存共享中所有的进程操作的是同一块共享内存。内存映射中每个进程在自己的虛拟地址空间中有一个独立的内存。
    4.数据安全不同。进程突然退出,共享内存还存在,内存映射区消失;运行进程的电脑死机、宕机,共享内存就没有了,而内存映射区的数据,由于磁盘文件中的数据还在,所以内存映射区的数据还存在
    5.生命周期不同。内存映射区:进程退出,内存映射区销毁。共享内存:进程退出,会自动取消与共享内存的关联,但是共享内存依然存在,必须手动删除或者关机。

守护进程

01 终端

  • 在UNIX系统中,用户通过终端登录系统后得到一个shell进程,这个终端成为shell进程的控制终端(Controlling Terminal),进程中,控制终端是保存在PCB中的信息,而fork()会复制PCB中的信息,因此由shell进程启动的其它进程的控制终端也是这个终端。
  • 默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
  • 在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl + C会产生SIGINT信号,Ctrl + \会产生SIGQUIT信号。

02 进程组

  • 进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持shell作业控制而定义的抽象概念,用户通过shell能够交互式地在前台或后台运行命令。
  • 进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID为该进程组的ID,新进程会继承其父进程所属的进程组ID。
  • 进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出当前进程组。进程组首进程无需是最后一个离开进程组的成员。

03 会话

  • 会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程ID会成为会话ID。新进程会继承其父进程的会话ID。
  • 一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
  • 在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
  • 当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。

04 进程组、会话、控制终端之间的关系

运行一下两条指令
find / 2 > /dev/null | wc -l &(&代表后台运行)
sort < longlist | uniq -c
会产生以下关系

05 进程组、会话操作函数

pid_t getpgrp(void);    //获取当前进程的进程组ID
pid_t getpgid(pid_t pid);    //获取指定进程的进程组ID
int setpgid(pid_t pid, pid_t pgid);    //设置进程组
pid_t getsid(pid_t pid);    //设置会话
pid_t setsid(void);

06 守护进程

  • 守护进程(Daemon Process),也就是通常说的Daemon进程(精灵进程),是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
  • 守护进程具备下列特征:
    1.生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
    2.它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如SIGINT、SIGQUIT)。
    3.Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd,远程连接sshd等。

07 守护进程的创建步骤

  1. 执行一个fork(),之后父进程退出,子进程继续执行。
  2. 子进程调用setsid()开启一个新会话。目的是脱离控制终端
  3. 设置进程的umask以确保当守护进程创建文件和目录时拥有所需的权限。(可选操作)
  4. 修改进程的当前工作目录,通常会改为根目录( / )。
  5. 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
  6. 在关闭了文件描述符0、1、2之后,守护进程通常会打开/dev/null并使用dup2()使所有这些描述符指向这个设备。
  7. 核心业务逻辑

说明:

  1. 第2步子进程调用setsid()开启一个新会话的目的是脱离控制终端,这种方式产生的新会话只要控制终端不与当前会话产生连接,当前进程就不拥有控制终端,从而确保了内核永远不会为守护进程自动生成任何控制信号以及不接收终端相关的信号(如SIGINT、SIGQUIT),这样不会让守护进程中断。
  2. 第2步中不用父进程而使用子进程创建新会话的原因是父进程创建的新会话的ID与原有的会话ID冲突,而子进程创建的新会话ID会设置为子进程的ID,不会产生冲突。
  3. 第1步中父进程如果不退出,运行完毕后会显示shell提示符,如:[root@Edu01 ~]# 。
  4. 第3步为可选操作
  5. 第4步的修改当前的工作目录,假设守护进程是在U盘中启动的,那么U盘就不能卸载了,必须一直插在电脑上,而根目录一般不会卸载,所以工作目录改为根目录。
  6. 第5步关闭文件描述符,子进程的文件描述符是从父进程继承而来,子进程的文件描述符0、1、2分别指向标准输入、标准输出、标准错误,如果不关闭,有可能会往终端输出内容,而父进程如果打开了一些文件,那么子进程就占用了这个文件所在的磁盘,这个文件所在磁盘就无法卸载,所以需要关闭。
  7. 文件描述符0、1、2重定向到/dev/null。文件描述符0、1、2不关闭而是重定向的原因是某些系统调用还是需要用到这三个文件描述符,如果关闭了,这些系统调用就会出错,/dev/null的特点是往这里写的数据都会被丢弃。

08 守护进程示例

//实现一个守护进程,每两秒钟获取当前时间并写入磁盘文件中

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h> 
#include <stdlib.h>
#include <string.h>

void work(int num)
{
    time_t tm = time(NULL);
    struct tm * loc = localtime(&tm);
    char * str = asctime(loc);
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd, str, strlen(str));
    close(fd);
}

int main()
{
    //1.执行个fork(), 父进程退出,子进程继续执行。
    pid_t pid = fork();
    if(pid > 0)
    {
        exit(0);
    }
    //2.子进程调用setsid()开启一个新会话
    setsid();
    //3.设置进程的umask
    umask(022);
    //4.修改进程的当前工作目录,由于是示例,所以就不修改到根目录了
    chdir("/root/linux/daemon/");
    //5.将文件描述符0、1、2重定向到/dev/null
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
    //6.核心业务逻辑

    //捕捉信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    //设置定时器
    struct itimerval val;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &val, NULL);
    while(1)
    {

    }
    return 0;
}
posted @ 2022-09-17 10:21  小肉包i  阅读(143)  评论(0)    收藏  举报