【Linux Server】二、Linux多进程开发

二、Linux多进程开发

1.为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个 PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体。

2.在 /usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以查看 struct task_struct 结构体定义。其内部成员有很多,我们只需要掌握以下部分即可:

  • 进程id:系统中每个进程有唯一的 id,用 pid_t 类型表示,其实就是一个非负整数
  • 进程的状态:有就绪、运行、挂起、停止等状态
  • 进程切换时需要保存和恢复的一些CPU寄存器
  • 述虚拟地址空间的信息
  • 描述控制终端的信息
  • 当前工作目录(Current Working Directory)
  • umask 掩码
  • 文件描述符表,包含很多指向 file 结构体的指针
  • 和信号相关的信息
  • 用户 id 和组 id
  • 会话(Session)和进程组
  • 进程可以使用的资源上限(Resource Limit)

当前系统的资源情况:ulimit -a

  1. 进程相关命令
  • 查看当前终端的命令
    tty
  • 查看进程
    ps aux / ajx
    a:显示终端上的所有进程,包括其他用户的进程
    u:显示进程的详细信息
    x:显示没有控制终端的进程
    j:列出与作业控制相关的信息
  • 执行ps aux命令结果中STAT参数意义:
    D 不可中断 Uninterruptible(usually IO)
    R 正在运行,或在队列中的进程
    S(大写) 处于休眠状态
    T 停止或被追踪
    Z 僵尸进程
    W 进入内存交换(从内核2.6开始无效)
    X 死掉的进程
    < 高优先级
    N 低优先级
    s 包含子进程
    • 位于前台的进程组
  • 实时显示进程动态
    top
    可以在使用 top 命令时加上 -d 来指定显示信息更新的时间间隔,在 top 命令执行后,可以按以下按键对显示的结果进行排序:
    M 根据内存使用量排序
    P 根据 CPU 占有率排序
    T 根据进程运行时间长短排序
    U 根据用户名来筛选进程
    K 输入指定的 PID 杀死进程
  • 杀死进程
    kill [-signal] pid
    kill –l // 列出所有信号
    kill –SIGKILL 进程ID // SIGKILL就是9
    kill -9 进程ID // 9就是SIGKILL
    killall name // 根据进程名杀死进程
  • 让进程在后台运行的命令
    ./a.out &
  • 后台运行的进程切换至前台的命令
    fg
  • 进程号和相关函数
    • 每个进程都由进程号来标识,其类型为pid_t(int类型),进程号的范围:0~32767。进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
    • 任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
    • 进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号。
    • 进程号和进程组相关函数:
      pid_t getpid(void);
      pid_t getppid(void);
      pid_t getpgid(pid_t pid);

4.进程创建
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。

    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork(void);
    返回值:
    fork的返回值会返回两次,一次是在父进程中,一次是在子进程中。所以,通过fork的返回值区分父进程和子进程。
    成功:子进程中返回0,父进程中返回子进程ID。
    失败:返回-1(在父进程中返回,表示创建子进程失败)
    失败的两个主要原因:
    1. 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置
    为 EAGAIN;
    2. 系统内存不足,这时 errno 的值被设置为 ENOMEM。

fork()以后,子进程的用户区数据和父进程一样,内核区也会拷贝过来,但是pid、ppid、信号集不一样。

5.读时共享、写时拷贝的原理

Linux 的 fork() 使用是通过写时拷贝实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只有在写入时才会复制地址空间(重新开辟一块内存),从而使各个进程拥有自己的地址空间。即资源的复制只有在写入时才会进行,在此之前,只有以只读的方式进行。

fork() 之后的父子进程共享文件,此时的 fork() 产生的子进程与父进程相同的文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。

6.GDB 多进程调试

  • 使用 GDB 调试的时候,GDB 默认只能跟踪一个进程,可以在 fork 函数调用之前,通过指令设置GDB调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程。
  • 设置调试父进程或者子进程:set follow-fork-mode [parent(默认)| child]
  • 显示当前跟踪的模式:show follow-fork-mode
  • 设置调试模式:set detach-on-fork [on | off]
    默认为 on,表示调试当前进程的时候,其它的进程继续运行,如果为 off,调试当前进程的时候,其它进程被 GDB 挂起。
  • 查看调试的进程:info inferiors
  • 切换当前调试的进程:inferior 编号
  • 使进程脱离 GDB 调试:detach inferiors 编号

7.exec 函数族介绍

  • exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。一般就是fork之后,在子进程里面调用exec函数。
  • exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。

8.exec 函数族

  • int execl(const char *path, const char arg, …/ (char *) NULL */);

    • 常用。
    • 例子execl(“mainApp”, “mainApp”, NULL); // 执行当前目录下可执行程序mainApp
    • 例子execl(“/bin/ps”, “ps”, “aux”, NULL); // (通过which ps可以知道ps的位置)执行shell命令 ps -aux
    • path是需要执行的文件路径或者名称
    • arg就是传给需要执行的文件的main函数的参数。而第一个参数和常用的main函数用法一样,传的是执行程序的名称;第二个参数开始才是程序执行所需要的参数列表;参数最后需要以NULL结束。
    • 该函数调用失败才会有返回值-1;调用成功的话,不会有返回值。
  • int execlp(const char *file, const char arg, … / (char *) NULL */);

    • 常用。
    • 例子execlp(“mainApp”, “mainApp”, NULL);
    • 例子execlp(“ps”, “ps”, “aux”, NULL);
    • 该函数会到环境变量中查找指定的可执行文件,如果找到了就执行,否则执行失败。
    • file是需要执行的可执行文件的文件名
  • int execle(const char *path, const char arg, …/, (char *) NULL, char *const envp[] */);

  • int execv(const char *path, char *const argv[]);

  • int execvp(const char *file, char *const argv[]);

  • int execvpe(const char *file, char *const argv[], char *const envp[]);

  • int execve(const char *filename, char *const argv[], char *const envp[]);

    • 前面6个函数都是标准C库的函数,而execve是Linux的函数。
    • filename即可执行程序的程序名,例如"mainApp"
    • argv中第一个参数一定要把路径写对。例如{“./mainApp”, “mainApp”,(char*)0};
    • 第三个参数envp数组参数不是用来查找可执行程序的,而是为可执行程序运行期间增加新的环境变量。例如{“PATH=/home/bread/Linux/code/lesson19”,(char*)0}
  • 函数名中的字母有如下含义:

    • l(list) 参数地址列表,以空指针结尾
    • v(vector) 存有各参数地址的指针数组的地址
    • p(path) 按 PATH 环境变量指定的目录搜索可执行文件
    • e(environment) 存有环境变量字符串地址的指针数组的地址

9.进程退出

// 标准C库函数
#include <stdlib.h>
void exit(int status);

// Linux系统函数
#include <unistd.h>
void _exit(int status);
  • 调用标准C库函数的exit函数实际上是先调用退出处理函数,然后刷新I/O缓冲关闭文件描述符,最后调用_exit系统调用,然后进程终止运行。
    • printf(“aaa\n”); // 因为有\n所以会刷新缓冲区
    • printf(“aaa”); // 因为没有\n所以数据会暂存缓冲区,调用_exit函数将不会打印。
  • status参数是进程退出时的一个状态信息,父进程回收子进程资源的时候可以获取到。

10.孤儿进程

  • 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程
    (Orphan Process)。
  • 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。
  • 因此孤儿进程并不会有什么危害。

11.僵尸进程

  • 每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的PCB没有办法自己释放掉,需要父进程去释放。
  • 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
  • 僵尸进程不能被 kill -9 杀死。这样就会导致一个问题,如果父进程不调用wait()或waitpid()的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

12.进程回收

  • 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
  • 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
  • wait() 和 waitpid() 函数的功能一样,区别在于,wait()函数会阻塞,waitpid()可以设置不阻塞,waitpid()还可以指定等待哪个子进程结束。
  • 注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。
参数:int *wstatus 进程退出时的状态信息,传入的是一个int类型的地址,这个是传出参数。
返回值:
    - 成功:返回被回收的子进程的id
    - 失败:-1 (所有的子进程都结束,调用函数失败)

调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.
    
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
    - pid:
        pid > 0 : 某个子进程的pid
        pid = 0 : 回收当前进程组的任意子进程    
        pid = -1 : 回收任意的子进程,waitpid(-1,&st,0)相当于wait(&st)  (最常用)
        pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
    - options:设置阻塞或者非阻塞
        0 : 阻塞(默认)
        WNOHANG : 非阻塞
    - 返回值:
        > 0 : 返回子进程的id
        = 0 : 在options=WNOHANG时才会返回0, 表示还有子进程活着
        = -1 :错误,或者没有子进程了

13.退出信息相关宏函数

  • WIFEXITED(status) 非0,进程正常退出
  • WEXITSTATUS(status) 如果宏为真,获取进程退出的状态(exit的参数)
  • WIFSIGNALED(status) 非0,进程异常终止
  • WTERMSIG(status) 如果宏为真,获取使进程终止的信号编号
  • WIFSTOPPED(status) 非0,进程处于暂停状态
  • WSTOPSIG(status) 如果宏为真,获取使进程暂停的信号的编号
  • WIFCONTINUED(status) 非0,进程暂停后已经继续运行
int st;
int ret = wait(&st);
if(-1 == ret)
    return;
if(WIFEXITED(st))
    printf("非正常退出状态码为:%d\n", WEXITSTATUS(st));

14.同一主机进程间通信

  • Unix进程间通信方式
    • 匿名管道
    • 有名管道
    • 信号
  • System V进程间通信方式、POSIX进程间通信方式
    • 消息队列
    • 共享内存
    • 信号量
      还有一种方式是内存映射。

15.管道的特点

  • 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
  • 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
  • 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
  • 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。管道其实是个环形队列。
  • 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
  • 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据。
  • 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。因为它们共享文件描述符(例如在fork之前创建管道)。
  • 自Linux2.6.11内核起,管道容量的大小默认是65536字节。我们可以使用fcntl函数来修改管道容量。
  1. 匿名管道的使用

管道也叫无名(匿名)管道,它是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。
例如:统计一个目录中文件的数目命令:ls | wc –l,为了执行该命令,shell 创建了两个进程来分别执行 ls 和 wc。(|是管道符,前面的进程通过管道发数据给后面的进程,后面的进程处理后把结果显示在界面上)

匿名管道是半双工的,不允许同时双向读写。因此,父进程只保留读端或写段,而将写段或者读端关闭;子进程也一样。

  • 创建匿名管道
    #include <unistd.h>
    int pipe(int pipefd[2]); // pipefd[0]是读,pipefd[1]是写.
  • 查看管道缓冲大小命令
    ulimit –a
  • 查看管道缓冲大小函数
    #include <unistd.h>
    long fpathconf(int fd, int name);
int pipefd[2];
pipe(pipefd);
long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
// size和命令ulimit –a的结果是一样的。

注意:匿名管道只能用于具有关系的进程之间的通信(父子进程、兄弟进程)。

管道默认是阻塞的:如果管道中没有数据,read阻塞;如果管道满了,write阻塞。

如果要实现双向的数据传输,就应该使用两个管道。

把管道设置为非阻塞:
例如把读端设置为非阻塞:
int flags = fcntl(pipefd[0], F_GETFL);
flags |= O_NONBLOCK;
fcntl(pipefd[0], F_SETFL, flags);

/*
    实现 ps aux | grep xxx 父子进程间通信
    
    子进程: ps aux, 子进程结束后,将数据发送给父进程
    父进程:获取到数据,过滤
    pipe()
    execlp()
    子进程将标准输出 stdout_fileno 重定向到管道的写端。  dup2
*/
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>

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) // 减1是因为字符串的结束符
        {
            // 过滤数据输出
            printf("%s", buf);
            memset(buf, 0, 1024);
        }
        wait(NULL);     // 回收子进程的资源
    } 
    else if(pid == 0)   // 子进程
    {
        close(fd[0]);   // 关闭读端

        // 文件描述符的重定向 stdout_fileno -> fd[1]
        dup2(fd[1], STDOUT_FILENO);
        
        // 执行 ps aux
        execlp("ps", "ps", "aux", NULL);
        perror("execlp");
        exit(0);
    } 
    else
    {
        perror("fork");
        exit(0);
    }
    return 0;
}

17.管道的读写特点

使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)

  • 所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
  • 如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
  • 如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE,通常会导致进程异常终止。
  • 如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。

总结:(下面的有名管道也是下面这些特点)

  • 读管道:
    • 管道中有数据,read返回实际读到的字节数。
    • 管道中无数据:写端被全部关闭,read返回0(相当于读到文件的末尾);写端没有完全关闭,read阻塞等待
  • 写管道:
    • 管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
    • 管道读端没有全部关闭:管道已满,write阻塞;管道没有满,write将数据写入,并返回实际写入的字节数

18.有名管道

  • 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。
  • 有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO,不相关的进程也能交换数据。
  • 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO的名称也由此而来:先入先出。
  • 有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于:
    • FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。
    • 当使用 FIFO 的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。
    • FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。

19.有名管道的使用

  • 通过命令创建有名管道
    mkfifo 名字
  • 通过函数创建有名管道
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
    • pathname是管道名称的途径
    • mode是文件的权限,和open文件的权限是一样的。可以是一个八进制的数。
  • 一旦使用 mkfifo 创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink 等。
  • FIFO 严格遵循先进先出(First in First out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 等文件定位操作。
  • 当只有写断程序(权限是只写)或者读端程序(权限是只读)在运行时,写断程序或者读端程序会在open管道的函数处阻塞。

20.内存映射

  • 概念:内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
  • 内存映射相关系统调用
    #include <sys/mman.h>  
    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);  
        - mmap函数用于申请一段内存空间。我们可以将这段内存作为进程间通信的共享内存也可以将文件直接映射到其中。  
        - addr是映射的内存的首地址,一般传NULL,由内核指定,然后通过返回值返回。  
        - length必须大于0,映射数据的长度。建议使用文件的长度(stat或lseek函数可以获取文件的长度)。  
        - prot是对申请的内存映射区的操作权限。  
            -PROT_EXEC :可执行的权限  
            -PROT_READ :读权限  
            -PROT_WRITE :写权限  
            -PROT_NONE :没有访问权限  
            要操作映射内存,必须要有读的权限。  
        - flags :  
            -MAP_SHARED :   映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项  
            -MAP_PRIVATE   :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)  
            -MAP_SHARED和MAP_PRIVATE是互斥的,不能同时指定。  
        -  fd: 需要映射的那个文件的文件描述符  
                -通过open得到,open的是一个磁盘文件  
                -注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。  
                    prot: PROT_READ                要求open:只读/读写   
                    prot: PROT_READ | PROT_WRITE   要求open:读写  
        - offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不偏移。  
        - 返回值:返回创建的内存的首地址。如果失败返回MAP_FAILED,其实就是(void *) -1。  
    int munmap(void *addr, size_t length);  
        - 功能:释放内存映射(释放由mmap创建的这段内存空间)  
        - 参数:  
            - addr : 要释放的内存的首地址  
            - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。  

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

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

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

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() 
{
    int fd = open("test.txt", O_RDWR);  // 打开一个文件(假设文件已经存在)
    int size = lseek(fd, 0, SEEK_END);  // 获取文件的大小

    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);    // 创建内存映射区
    
    // 这里也可以使用匿名内存映射区(不需要文件实体进程一个内存映射)
    // 匿名内存映射只能用于父子进程之间的通信
    // 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(0);
    }

    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, "helloworld!");
    }

    munmap(ptr, size);  // 关闭内存映射区
    return 0;
}

22.内存映射的注意事项

  • 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
    答:void * ptr = mmap(…);
    ptr++; 可以对其进行++操作
    munmap(ptr, len); // 错误,要保存原地址

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

  • 如果文件偏移量为1000会怎样?
    答:偏移量必须是4K的整数倍,返回MAP_FAILED

  • mmap什么情况下会调用失败?
    答:第二个参数:length = 0
    第三个参数:prot只指定了写权限。
    第三个参数:prot指定了读写权限(prot PROT_READ | PROT_WRITE),而第五个参数fd通过open函数打开时指定的是 O_RDONLY / O_WRONLY

  • 可以open的时候O_CREAT一个新文件来创建映射区吗?
    答:可以的,但是创建的文件的大小如果为0的话,肯定不行。
    可以对新的文件进行扩展用lseek()或truncate()

  • mmap后关闭文件描述符,对mmap映射有没有影响?
    答:int fd = open(“XXX”);
    mmap(,fd,0);
    close(fd);
    映射区还存在,创建映射区的fd被关闭,没有任何影响。

  • 对ptr越界操作会怎样?
    答:void * ptr = mmap(NULL, 100,);
    4K
    越界操作操作的是非法的内存 -> 段错误

23.通过内存映射实现拷贝文件(只能拷贝小文件)

#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() 
{
    int fd = open("text.txt", O_RDWR);   // 打开原始的文件
    if(fd == -1) 
    {
        perror("open");
        exit(0);
    }
   
    int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);  // 创建一个新文件(拓展该文件)
    if(fd1 == -1) 
    {
        perror("open");
        exit(0);
    }
    
    int len = lseek(fd, 0, SEEK_END);   // 获取原始文件的大小
    truncate("cpy.txt", len);   // 对新创建的文件进行拓展
    write(fd1, " ", 1);     // 如果不写的话它是拓展不了的。

    // 分别做内存映射
    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(0);
    }
    if(ptr1 == MAP_FAILED) 
    {
        perror("mmap");
        exit(0);
    }
    
    memcpy(ptr1, ptr, len);     // 内存拷贝
    
    // 释放资源,后来创建的先释放的原则(就有点像继承时的析构)
    munmap(ptr1, len);
    munmap(ptr, len);
    close(fd1);
    close(fd);
    return 0;
}

24.信号的概念

  • 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
  • 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
    • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号。
    • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
    • 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的CPU时间超限,或者该进程的某个子进程退出。
    • 运行 kill 命令或调用 kill 函数。
  • 使用信号的两个主要目的是:
    • 让进程知道已经发生了一个特定的事情。
    • 强迫进程执行它自己代码中的信号处理程序。
  • 信号的特点:
    • 使用简单
    • 不能携带大量信息
    • 满足某个特定条件才发送
    • 优先级比较高
  • 查看系统定义的信号列表:kill –l

25.Linux 信号一览表(前 31 个信号为常规信号,其余为实时信号)

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

26.信号的 5 种默认处理动作

  • 查看信号的详细信息:man 7 signal
  • 信号的 5 种默认处理动作
    • Term 终止进程
    • Ign 当前进程忽略掉这个信号
    • Core 终止进程,并生成一个Core文件
    • Stop 暂停当前进程
    • Cont 继续执行当前被暂停的进程
  • 信号的几种状态:产生、未决(还没有处理的状态)、递达
  • SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。

27.Core文件

  • 文件内容是程序运行过程中的错误信息
  • 生成core文件
    • ulimit -a 看到core file size是0
    • ulimit -c 1024 把core file size设置为1024或者直接设置为unlimited
    • gcc test.c -g
    • ./a.out
    • 如果a.out异常退出例如报"段错误(核心已转储)",然后就生成了一个core文件
    • gdb a.out
    • (gdb) core-file core 看错误信息

28.信号相关的函数

* int kill(pid_t pid, int sig);
    - 给任何的进程或进程组pid,发送任何的信号sig
    - pid :
        > 0 : 将信号发送给指定的进程  
        = 0 : 将信号发送给当前的进程组所有的进程  
        = -1 : 将信号发送给每一个有权限接收这个信号的进程  
        < -1 : 这个pid=某个进程组的ID取反(取负数)
    - sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号  
    - 例如:  
        kill(getppid(), 9);  
        kill(getpid(), 9);  

* int raise(int sig);
    - 给当前进程发送信号
    
* void abort(void);
    - 发送SIGABRT信号给当前的进程(默认是杀死当前进程)。相当于kill(getpid(), SIGABRT);
    
* unsigned int alarm(unsigned int seconds);
    - 设置定时器。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM
    - seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。取消一个定时器,通过alarm(0)。
    - 返回值:之前没有定时器,返回0;之前有定时器,返回之前定时器的剩余时间。
    - SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。  
        alarm(10);  -> 返回0  
        过了1秒  
        alarm(5);   -> 返回9  
    - alarm函数是非阻塞的。
    - 定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。

* 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秒后,每隔2秒定时一次:则it_value是10,it_interval是2.
    - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
    - 返回值:成功 0;失败 -1 并设置错误号
    - setitimer函数是非阻塞的。

29.信号捕捉函数

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

* 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; // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
        int        sa_flags;    // 使用哪一个信号处理对捕捉到的信号进行处理。这个值可以是0表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
        void     (*sa_restorer)(void);// 被废弃掉了
    };
    - 系统处理完临时阻塞信号集之后,又会回到系统的阻塞信号集。
* 信号处理函数的原型如下:
    #include <signal.h>
    typedef void (*_sighandler_t) (int);
    - 信号处理函数应该是可重入的,否则很容易引发一些竞态条件。因此在信号处理函数中严禁调用一些不安全的函数。

30.信号集

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

31.阻塞信号集和未决信号集

  • 用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
  • 信号产生但是没有被处理 (未决)
    • 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
    • SIGINT信号状态被存储在第二个标志位上。这个标志位的值为0, 说明信号不是未决状态;这个标志位的值为1, 说明信号处于未决状态。
  • 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
    • 阻塞信号集默认不阻塞任何的信号
    • 如果想要阻塞某些信号需要用户调用系统的API
  • 在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
    • 如果没有阻塞,这个信号就被处理
    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
  • 常规信号的处理不是进队列,假设在A信号处理的过程中,又发生了一次A信号,此时改变未决信号集的A信号标记位为1,然后又发生了一次A信号,那么等A信号处理完之后,系统会再调用一次A信号的处理函数(虽然后面又发生了两次A信号,但只会执行一次)。但是实时信号的处理是进队列的(34~64号)。

32.信号集相关的函数

以下信号集相关的函数都是对自定义的信号集进行操作。
* 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 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,传出参数,保存的是内核中的未决信号集中的信息。

33.SIGCHLD信号

  • SIGCHLD信号产生的条件
    • 子进程终止时
    • 子进程接收到 SIGSTOP 信号停止时
    • 子进程处在停止态,接受到SIGCONT后唤醒时
  • 以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号
  • 使用SIGCHLD信号解决僵尸进程的问题。

34.网络编程相关的信号

  • SIGHUP
    • 当挂起进程的控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件。
  • SIGPIPE
    • 默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。
  • SIGURG
    • 在Linux环境中,内核通知应用程序带外数据到达主要有两种方法:一种是I/O复用技术,select等系统调用在接收到带外数据时将返回,并向应用程序报告socket上的异常事件;另一种方法就是使用SIGURG信号。
    • 使用SIGURG信号之前,我们必须设置socket的宿主进程或进程组。

35.统一信号源

信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。

我们需要使用I/O复用系统调用来监听管道的读端文件描述符上的可读时间。如此一来,信号事件就能和其他I/O事件一样被处理,即统一事件源。

36.共享内存

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

37.共享内存使用步骤

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

38.共享内存操作函数

* 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
    - 返回值:
        -失败:-1 并设置错误号
        -成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
* 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:需要设置或者获取的共享内存的属性信息
        -cmd为IPC_STAT时: buf用来存储数据,传出参数
        -cmd为IPC_SET时: buf中需要初始化数据,设置到内核中
        -cmd为IPC_RMID时: buf没有用,传NULL
* key_t ftok(const char *pathname, int proj_id);
    - 功能:根据指定的路径名和int值,生成一个共享内存的key
    - pathname:指定一个必须存在的路径
    - proj_id: int类型的值,但是只会使用其中的1个字节。范围:0-255, 一般指定一个字符 'a'

39.共享内存常见问题

  • 操作系统如何知道一块共享内存被多少个进程关联?

    • 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch。shm_nattach 记录了关联的进程个数
  • 可不可以对共享内存进行多次删除shmctl

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

40.共享内存和内存映射的区别

  • 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
  • 共享内存效果更高
  • 内存
    • 所有的进程操作的是同一块共享内存。
    • 内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
  • 数据安全
    • 进程突然退出
      共享内存还存在。内存映射区消失
    • 运行进程的电脑死机,宕机了
      数据存储在共享内存中,没有了。内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
  • 生命周期
    • 内存映射区:进程退出,内存映射区销毁。
    • 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机。
      如果一个进程退出,会自动和共享内存进行取消关联。

41.共享内存操作命令

  • ipcs 用法
    • ipcs -a // 打印当前系统中所有的进程间通信方式的信息
    • ipcs -m // 打印出使用共享内存进行进程间通信的信息
    • ipcs -q // 打印出使用消息队列进行进程间通信的信息
    • ipcs -s // 打印出使用信号进行进程间通信的信息
  • ipcrm 用法
    • ipcrm -M shmkey // 移除用shmkey创建的共享内存段
    • ipcrm -m shmid // 移除用shmid标识的共享内存段(连接数不变,键变全0)
    • ipcrm -Q msgkey // 移除用msqkey创建的消息队列
    • ipcrm -q msqid // 移除用msqid标识的消息队列
    • ipcrm -S semkey // 移除用semkey创建的信号
    • ipcrm -s semid // 移除用semid标识的信号

42.终端

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

43.进程组

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

44.会话

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

45.进程组、会话操作函数

  • pid_t getpgrp(void);
  • pid_t getpgid(pid_t pid);
    • 获取指定进程的PGID。
  • int setpgid(pid_t pid, pid_t pgid);
  • pid_t getsid(pid_t pid);
  • pid_t setsid(void);
    • 该函数不能由进程组的首领进程调用。

46.守护进程

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

47.守护进程的创建步骤

  • 执行一个 fork(),之后父进程退出,子进程继续执行。(为了进程组id不重复、会话id不冲突)
  • 子进程调用 setsid() 开启一个新会话。(脱离控制终端)
  • 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。
  • 修改进程的当前工作目录,通常会改为根目录(/)。
  • 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
  • 在关闭了文件描述符0、1、2之后,守护进程通常会打开/dev/null 并使用dup2() 使所有这些描述符指向这个设备。
  • 核心业务逻辑
// 将服务器程序以守护进程的方式运行
bool daemonize()
{
    // 创建子进程,关闭父进程,这样可以使程序在后台运行
    pid_t pid = fork();
    if(pid < 0)
        return false;
    else if(pid > 0)
        exit(0);
        
    // 设置文件权限掩码。
    // 当进程创建新文件(使用open(const char *pathname, int flags, mode_t mode)系统调用)时,文件的权限将是 mode & 0777
    umask(0);
    
    // 创建新的会话,设置本进程为进程组的首领
    pit_t sid = setsid();
    if(sid < 0)
        return false;
    
    // 切换工作目录
    if(chdir("/") < 0)
        return false;
    
    // 关闭标准输入设备、标准输出设备和标准错误输出设备    
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    
    // 关闭其他已经打开的文件描述符,代码省略
    // 将标准输入、标准输出和标准错误输出都定向到/dev/null文件
    open("/dev/null", O_RDONLY);
    open("/dev/null", O_RDWR);
    open("/dev/null", O_RDWR);
    
    return true;
}

实际上,Linux提供了完成同样功能的库函数:
#include <unistd.h>
int daemon(int nochdir, int noclose);
其中,nochdir参数用于指定是否改变工作目录。如果给它传递0,则工作目录将被设置为“/”(根目录),否则继续使用当前工作目录。
noclose参数为0时标准输入、标准输出和标准错误输出都被重定向到/dev/null文件,否则依然使用原来的设备。

48.用守护进程获取系统时间,写入磁盘文件

#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() 
{
    pid_t pid = fork();         // 创建子进程,退出父进程
    if(pid > 0) 
        exit(0);

    setsid();                   // 将子进程重新创建一个会话
    umask(022);                 // 设置掩码
    chdir("/home/bread/");      // 更改工作目录

    int fd = open("/dev/null", O_RDWR); // 关闭、重定向文件描述符
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 下面是业务逻辑

    // 捕捉定时信号
    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)    // 不让进程结束
        sleep(10);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值