Linux多进程开发(四)

1.信号

信号概念

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

发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:

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

使用信号的两个主要目的:

  • 让进程知道已经发生了一个特定的事情
  • 强迫进程执行它自己代码中的信号处理程序

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

查看系统定义的信号列表:kill –l
在这里插入图片描述
前 31 个信号为常规信号,其余为实时信号

Linux 信号一览表:

编号信号名称对应时间默认动作
2SIGINT当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号终止进程
3SIGQUIT用户按下<Ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号终止进程
9SIGKILL无条件终止进程。该信号不能被忽略,处理和阻塞终止进程,可以杀死任何进程
11SIGSEGV指示进程进行了无效内存访问(段错误)终止进程并产生core文件
13SIGPIPEBroken pipe向一个没有读端的管道写数据终止进程
17SIGCHLD子进程结束时,父进程会收到这个信号忽略这个信号
18SIGCONT如果进程已停止,则使其继续运行继续 / 忽略
19SIGSTOP停止进程的执行。该信号不能被忽略,处理和阻塞终止进程

信号的处理动作:

查看信号的详细信息:man 7 signal

信号的五种默认处理动作
进程收到信号后必须会做五个处理动作之一

Term   终止进程
Ign    当前进程忽略掉这个信号
Core   终止进程,并生成一个Core文件
Stop   暂停当前进程
Cont   继续执行当前被暂停的进程

信号的几种状态:产生、未决、递达

SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。

信号相关函数

int kill(pid_t pid, int sig);
int raise(int sig);
void abort(void);

unsigned int alarm(unsigned int seconds);

int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);

sighandler_t signal(int signum, sighandler_t handler);
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

kill、raise、abort函数

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
    功能:给任何的进程或者进程组pid, 发送信号sig
    参数:
        - pid :
               > 0 : 将信号发送给指定的进程
               = 0 : 将信号发送给当前的进程组
               = -1 : 将信号发送给每一个有权限接收这个信号的进程
               < -1 : 这个pid=某个进程组的ID取反
        - sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
        
int raise(int sig);
    功能:给当前进程发送信号
    参数:
        - sig : 要发送的信号
    返回值:成功返回0,失败返回非0
    用kill实现raise:kill(getpid(), sig);   

void abort(void);
    功能: 发送SIGABRT信号给当前的进程,杀死当前进程
    用kill实现abort:kill(getpid(), SIGABRT);
int main() {

    pid_t pid = fork();

    if(pid == 0) {         
        int i = 0;
        for(i = 0; i < 5; i++) {              //打印五次child process
            printf("child process\n");
            sleep(1);
        }

    } else if(pid > 0) {
        printf("parent process\n");      
        sleep(2);
        printf("kill child process now\n");
        kill(pid, SIGINT);                   //向子进程发送终止信号
    }

    return 0;
}

可以看到最后结果子进程并没有打印出五次就被终止
在这里插入图片描述

alarm函数

定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
    功能:设置定时器(闹钟)。
          函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM
    参数:
        seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
                 取消一个定时器,通过alarm(0)。
    返回值:
           - 之前没有定时器,返回0
           - 之前有定时器,返回之前的定时器剩余的时间

    - SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
        alarm(10);  -> 返回0
        过了1alarm(5);   -> 返回9

alarm(100) -> 该函数是不阻塞的

示例:

int main() {

    int seconds = alarm(5);
    printf("seconds = %d\n", seconds);  // 返回0

    sleep(2);
    seconds = alarm(2);    // 不阻塞
    printf("seconds = %d\n", seconds);  // 返回上次设置的计时器剩余的时间3
    while(1) {             //一直循环,但是定时(2s)后会程序终止
    }

    return 0;
}

在这里插入图片描述

setitimer定时器函数

alarm函数只能定时一次,setitimer定时器函数可以进行周期性的定时

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
    功能:设置定时器(闹钟)。可以替代alarm函数。精度微秒us,可以实现周期性定时
    参数:
        - 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;     // 微秒    
            };10秒后(it_value),每个2秒定时一次(it_interval)
           
        - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
        
    返回值:成功返回0,;失败返回-1,并设置错误号

示例:

// 过3秒以后,每隔2秒钟定时一次
int main() {

    struct itimerval new_value;
    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;     //如果不设置微秒的值就会默认随机值
    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;        //同上
    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了...\n");

    getchar();                             //获取键盘录入防止程序直接return 0

    return 0;
}

在这里插入图片描述

signal信号捕捉函数

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
    功能:设置某个信号的捕捉行为
    参数:
        - signum: 要捕捉的信号
        - handler: 捕捉到信号要如何处理
                   - SIG_IGN : 忽略信号
                   - SIG_DFL : 使用信号默认的行为
                   - 回调函数 :  这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
                     回调函数:
                             - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
                             - 不是程序员调用,而是当信号产生,由内核调用
                             - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
    返回值:
           成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
           失败,返回SIG_ERR,设置错误号
            
SIGKILL、SIGSTOP不能被捕捉,不能被忽略

示例:

void myalarm(int num) {    //根据void (*sighandler_t)(int)写回调函数
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

// 过3秒以后,每隔2秒钟定时一次
int main() {

    // 注册信号捕捉,要在定时器之前
    // signal(SIGALRM, SIG_IGN);     //捕捉SIGALRM信号,
    // signal(SIGALRM, SIG_DFL);     //相当于什么都没有设置
    // void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
    signal(SIGALRM, myalarm);

    struct itimerval new_value;
    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;
    // 设置延迟的时间,3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;
    int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
    printf("定时器开始了...\n");

    getchar();

    return 0;
}

在这里插入图片描述

信号集

许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t

在 PCB 中有两个非常重要的信号集:阻塞信号集(阻塞信号递达)和未决信号集(记录没有递达的信号)。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。

信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
在这里插入图片描述

虚拟地址空间分为内核区和用户区,内核区的PCB进程控制块中保存了进程相关的信息(文件描述符表、PID、PPID等),未决信号集和阻塞信号集也在PCB中。

未决信号集和阻塞信号集的工作方式:

1.用户通过键盘  Ctrl + C, 产生2号信号SIGINT (信号被创建)

2.信号产生但是没有被处理 (未决)
    - 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
    - SIGINT信号状态被存储在第二个标志位上
        - 这个标志位的值为0, 说明信号不是未决状态
        - 这个标志位的值为1, 说明信号处于未决状态
    
3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
    - 阻塞信号集默认不阻塞任何的信号
    - 如果想要阻塞某些信号需要用户调用系统的API

4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
    - 如果没有阻塞,这个信号就被处理
    - 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

信号集相关函数

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);

sigemptyset、sigfillset、sigaddset、sigdelset、sigismember函数

以下信号集相关的函数都是对自定义的信号集进行操作

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);
    功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
    参数:
        - set:传出参数,需要操作的信号集
        - signum:需要设置不阻塞的那个信号
    返回值:成功返回0, 失败返回-1

int sigismember(const sigset_t *set, int signum);
    功能:判断某个信号是否阻塞
    参数:
        - set:需要操作的信号集
        - signum:需要判断的那个信号
    返回值:
           1 : signum被阻塞
           0 : signum不阻塞
          -1 : 失败

示例:

int main() {

    sigset_t set;                           //创建一个信号集
    sigemptyset(&set);                      //清空信号集的内容
    //判断SIGINT是否在信号集set里
    int ret = sigismember(&set, SIGINT);    
    if(ret == 0) {                          //0 : signum不阻塞
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {                   //1 : signum被阻塞
        printf("SIGINT 阻塞\n");
    }
    sigaddset(&set, SIGINT);                //添加信号到信号集中
    sigaddset(&set, SIGQUIT);               //添加信号到信号集中
    // 判断SIGINT是否在信号集中
    ret = sigismember(&set, SIGINT);
    if(ret == 0) {
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGINT 阻塞\n");
    }
    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGQUIT 阻塞\n");
    }
    sigdelset(&set, SIGQUIT);              //从信号集中删除一个信号
    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGQUIT 阻塞\n");
    }

    return 0;
}

在这里插入图片描述

sigprocmask、sigpending函数

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
    参数:
         - how : 如何对内核阻塞信号集进行处理
                 SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
                    假设内核中默认的阻塞信号集是mask, 那么就mask | set
                 SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
                    mask &= ~set
                 SIG_SETMASK:覆盖内核中原来的值
            
         - set :已经初始化好的用户自定义的信号集
         - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
    返回值:
            成功返回0
            失败返回-1,并设置错误号:EFAULT、EINVAL

int sigpending(sigset_t *set);
    功能:获取内核中的未决信号集
    参数:set,传出参数,保存的是内核中的未决信号集中的信息

示例:

int main() {

    // 设置2、3号信号阻塞,这两个信号可以通过键盘产生
    sigset_t set;                                 //创建一个信号集
    sigemptyset(&set);                            //清空信号集的内容
    sigaddset(&set, SIGINT);                      //将2号和3号信号添加到信号集中
    sigaddset(&set, SIGQUIT);
    sigprocmask(SIG_BLOCK, &set, NULL);           //修改内核中的阻塞信号集

    int num = 0;
    while(1) {
        num++;
        // 获取当前的未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);
        // 遍历前32位
        for(int i = 1; i <= 31; i++) {
            if(sigismember(&pendingset, i) == 1) {
                printf("1");
            }else if(sigismember(&pendingset, i) == 0) {
                printf("0");
            }else {
                perror("sigismember");
                exit(0);
            }
        }
        printf("\n");
        sleep(1);
        if(num == 10) {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }

    }
    return 0;
}

在这里插入图片描述

sigaction信号捕捉函数

功能和signal信号捕捉函数差不多

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
        功能:检查或者改变信号的处理。信号捕捉
        参数:
            - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
            - act :捕捉到信号之后的处理动作
            - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递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);
    };

在这里插入图片描述

SIGCHLD信号

SIGCHLD信号产生的条件:
子进程终止时;
子进程接收到 SIGSTOP 信号停止时;
子进程处在停止态,接受到SIGCONT后唤醒时

以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号

可以通过SIGCHLD信号解决僵尸进程的问题,当父进程捕捉到SIGCHLD信号就可以调用wait()回收子进程的资源。

2.共享内存

共享内存概念

共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。

与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。

使用共享内存

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

共享内存操作函数

int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
key_t ftok(const char *pathname, int proj_id);
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
    功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
          新创建的内存段中的数据都会被初始化为0
    参数:
        - key : key_t类型是一个整形,通过这个值找到或者创建一个共享内存(一般使用16进制表示,非0)   
        - size: 共享内存的大小
        - shmflg: 共享内存的属性
                  - 访问权限
                  - 附加属性:创建/判断共享内存是不是存在
                            - 创建:IPC_CREAT
                            - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
                              如IPC_CREAT | IPC_EXCL | 0664(多个flg按位或)
    返回值:
           成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个ID
           失败:-1 并设置错误号


void *shmat(int shmid, const void *shmaddr, int shmflg);
    功能:和当前的进程进行关联
    参数:
         - shmid : 共享内存的标识(ID),由shmget返回值获取
         - 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: 标记共享内存需要被销毁
        - buf:需要设置或者获取的共享内存的属性信息
               - IPC_STAT : buf存储数据
               - IPC_SET : buf中需要初始化数据,设置到内核中
               - IPC_RMID : 没有用,NULL
    返回值:成功返回共享内存ID,失败返回-1并设置错误号

key_t ftok(const char *pathname, int proj_id);
    功能:根据指定的路径名,和int值,生成一个共享内存的key
    参数:
        - pathname:指定一个存在的路径
        - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
                   范围 : 0-255  一般指定一个字符 'a'
    返回值:成功返回生成的key值,失败返回-1并设置错误号

示例:
创建一个write.c,向共享内存中写入数据:

int main() {    

    //创建一个共享内存
    int shmid = shmget(100, 4096, IPC_CREAT|0664);
    printf("shmid : %d\n", shmid);
    //和当前进程进行关联
    void * ptr = shmat(shmid, NULL, 0);
    //定义字符串
    char * str = "helloworld";
    //写数据
    memcpy(ptr, str, strlen(str) + 1);

    printf("按任意键继续\n");
    getchar();

    //解除关联
    shmdt(ptr);
    //删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

创建一个read.c,向共享内存中读取数据:

int main() {    

    //获取一个共享内存
    int shmid = shmget(100, 0, IPC_CREAT);
    printf("shmid : %d\n", shmid);
    //和当前进程进行关联
    void * ptr = shmat(shmid, NULL, 0);
    //读数据
    printf("%s\n", (char *)ptr);
    
    printf("按任意键继续\n");
    getchar();

    //解除关联
    shmdt(ptr);
    //删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

在这里插入图片描述
在这里插入图片描述

共享内存操作命令

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_nattach 记录了关联的进程个数。

问题2:可不可以对共享内存进行多次删除 shmctl
可以,因为shmctl标记要删除的共享内存,不是直接删除。
当和共享内存关联的进程数为0的时候,就真正被删除;
当共享内存的key为0的时候,表示共享内存被标记删除了;
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。

共享内存和内存映射的区别:
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)

2.共享内存效果更高

3.共享内存,所有的进程操作的是同一块共享内存;内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存

4.进程突然退出,共享内存还存在、内存映射区消失;
运行进程的电脑死机,在共享内存中的数据会丢失,内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在

5.内存映射区:进程退出,内存映射区销毁
共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机。 如果一个进程退出,会自动和共享内存进行取消关联。

3.守护进程

终端

在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为 shell 进程的控制终端(Controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由 shell 进程启动的其它进程的控制终端也是这个终端。

默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。

在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl + C 会产生 SIGINT 信号,Ctrl + \ 会产生 SIGQUIT 信号。

进程组

进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。

进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程 ID 为该进程组的 ID,新进程会继承其父进程所属的进程组 ID。

进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。

会话

会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。

一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。

在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。

当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。

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

在终端中输入find / 2 > /dev/null | wc -l &,查找2、重定向到/dev/null,|为管道符,wc -l为统计,&为后台运行
在终端中输入sort < longlist | uniq -c
在这里插入图片描述

进程组、会话操作函数

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

守护进程

守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。

守护进程具备下列特征:
生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)。

Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等

创建守护进程

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值