• 深入浅出-多进程编程


    进程前言

    1.复制进程映像的fork 系统调用和替换进程映像的exec系列系统调用。僵尸进程以及如何避免僵尸进程。

    2.进程间通信(Inter-Process Communication,IPC)最简单的方式:管道。

    3.3种System V进程间通信方式:信号量、消息队列和共享内存。它们都是由AT&TSystem V2版本的UNIX引入的,所以统称为System v IPC.

    4.在进程间传递文件描述符的通用方法:通过UNIX本地域socket传递特殊的辅助数据

    fork and exec 系列系统调用

    #include
    #include
    
    pid_t fork(void);
    
    • 1
    • 2
    • 3
    • 4

    该函数的每次调用都返回两次,在父进程中返回的是子进程的PID,在子进程中则返回0。该返回值是后续代码判断当前进程是父进程还是子进程的依据。fork调用失败时返回-1,并设置errno。

    fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的PPID被设置成原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。

    子进程的代码与父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用的是所谓的写时复制(copy on writte),即只有在任一进程(父进程或子进程〉对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。即便如此,如果我们在程序中分配了大量内存,那么使用fork 时也应当十分谨慎,尽量避免没必要的内存分配和数据复制。

    此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。

    有时我们需要在子进程中执行其他程序,即替换当前进程映像,这就需要使用如下exec系列函数之一:

          #include 
    
           extern char **environ;
    
           int execl(const char *path, const char *arg, ...);
           int execlp(const char *file, const char *arg, ...);
           int execle(const char *path, const char *arg,
                      ..., 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[]);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    path参数指定可执行文件的完整路径,file参数可以接受文件名,该文件的具体位置则在环境变量PATH中搜寻。arg 接受可变参数,argv则接受参数数组,它们都会被传递给新程序(path或file指定的程序)的main函数。envp参数用于设置新程序的环境变量。如果未设置它,则新程序将使用由全局变量environ指定的环境变量。

    在这里插入图片描述

    该系列函数辨识方法
    该系列函数都以“exec”为前缀,后面的字母有各自固定的含义,可以根据这点来进行区分,而无需强行记忆。看下图详解

    在这里插入图片描述
    exec系列函数关系剖析
    在这里插入图片描述
    注意事项:

    如果代码想下图这样写,因为exec函数执行出错,但是后续代码仍然会被执行,可是:当前进程的内存空间(堆、栈、数据区)可能已经被破坏,所以这种写法是不妥的!
    在这里插入图片描述
    上图代码不妥,应该修改为下图方式,即设置进程退出:
    在这里插入图片描述
    参考链接实战

    僵尸进程

    对于多进程程序而言,父进程一般需要跟踪子进程的退出状态。因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)。在子进程结束运行之后,父进程读取其退出状态之前,我们称该子进程处于僵尸态。另外一种使子进程进人僵尸态的情况是﹔父进程结束或者异常终止,而子进程继续运行。此时子进程的PPID将被操作系统设置为1,即init进程。init进程接管了该子进程,并等待它结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸态。

    由此可见,无论哪种情况,如果父进程没有正确地处理子进程的返回信息,子进程都将停留在僵尸态,并占据着内核资源。这是绝对不能容许的,毕竟内核资源有限。下面这对函数在父进程中调用,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或者使子进程的僵尸态立即结束:

    #include
    #include
    pid_t wait(int* stat_loc);
    pid_t waitpid(pid_t pid,int* stat_loc,int options);
    
    • 1
    • 2
    • 3
    • 4

    wait函数将阻塞进程,直到该进程的某个子进程结束运行为止。它返回结束运行的子进PID,并将子进程的退出状态信息储存在stat_loc参数指向的内存中
    子进程状态信息
    在这里插入图片描述
    wait函数的阻塞特性显然不是服务器程序期望的,而 waitpid函数解决了这个问题。waitpid 只等待由pid参数指定的子进程。如果pid取值为-1,那么它就和 wait函数相同,即等待任意一个子进程结束。stat_loc参数的含义和wait函数的stat_loc参数相同。options参数可以控制waitpid函数的行为。该参数最常用的取值是WNOHANG。当options的取值是WNOHANG时,waitpid调用将是非阻塞的:如果pid指定的目标子进程还没有结束或意外终止,则waitpid立即返回0﹔如果目标子进程确实正常退出了,则waitpid返回该子进程的PID。waitpid 调用失败时返回-1并设置errno.

    这个博客,要在事件已经发生的情况下执行非阻塞调用才能提高程序的效率。对waitpid函数而言,我们最好在某个子进程退出之后再调用它。那么父进程从何得知某个子进程已经退出了呢?这正是SIGCHLD信号的用途。当一个进程结束时,它将给其父进程发辽一个SIGCHLD信号。因此,我们可以在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以“彻底结束”一个子进程。

    管道(有关联的进程)

    管道能在父、子进程间传递数据,利用的是fork 调用之后两个管道文件描述符(fd[0]和fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1]。比如,我们要使用管道实现从父进程向子进程写数据,下图
    在这里插入图片描述

    如果要实现父、子进程之间的双向数据传输,就必须使用两个管道。第6章中我们还介绍过,socket编程接口提供了一个创建全双工管道的系统调用: socketpair。

    信号量

    信号量原语

    当多个进程同时访问系统上的某个资源的时候,比如同时写一个数据库的某条记录,或者同时修改某个文件,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。通常,程序对共享资源的访问的代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件。我们称这段代码为关键代码段,或者临界区。对进程同步,也就是确保任一时刻只有一个进程能进人关键代码段。

    要编写具有通用目的的代码,以确保关键代码段的独占式访问是非常困难的。有两个名为Dekker算法和 Peterson算法的解决方案,它们试图从语言本身(不需要内核支持)解决并发问题。但它们依赖于忙等待,即进程要持续不断地等待某个内存位置状态的改变。这种方式下CPU利用率太低,显然是不可取的。

    Dijkstra提出的信号量(Semaphore)概念是并发编程领域迈出的重要一步。信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作﹔等待(wait)和信号(signal)。不过在Linux/UNIX中,“等待”和“信号”都已经具有特殊的含义,所以对信号量的这两种操作更常用的称呼是P、V操作。这两个字母来自于荷兰语单词passeren(传递,就好像进人临界区)和vrijgeven(释放,就好像退出临界区)。假设有信号量SV,则对它的P、V操作
    含义如下:
    P(SV),如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1。

    信号量的取值可以是任何自然数。但最常用的、最简单的信号量是二进制信号量,它只能取О和1这两个值。本书仅讨论二进制信号量。使用二进制信号量同步两个进程,以确保关键代码段的独占式访问的一个典型例子下图:
    在这里插入图片描述
    在图13-2中,当关键代码段可用时,二进制信号量SV的值为1,进程A和B都有机会进入关键代码段。如果此时进程A执行了P(sv)操作将SV减1,则进程B若再执行P(SV)操作就会被挂起。直到进程A离开关键代码段,并执行v(sV)操作将SV加1,关键代码段才重新变得可用。如果此时进程B因为等待sv而处于挂起状态,则它将被唤醒,并进入关键代码段。同样,这时进程A如果再执行P(SV)操作,则也只能被操作系统挂起以等待进程B退出关键代码段。

    注意
    使用一个普通变量来模拟二进制信号量是行不通的,因为所有高级语言都没有一个原子操作可以同时完成如下两步操作﹔检测变量是否为true/false,如果是则再将它设置为falsc/true。

    semget调用

    目的:创立一个新的信号量集或者获取一个已经存在的信号量集合

    #include
    int semget(key_t key,int num_sems,int sem_flags);
    
    
    • 1
    • 2
    • 3

    key参数是一个键值,用来标识一个全局唯一的信号量集,就像文件名全局唯-一地标识一个文件一样。要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量。
    num_sems参数指定要创建/获取的信号量集中信号量的数目。如果是创建信号量,则该值必须被指定:如果是获取已经存在的信号量,则可以把它设置为0。

    sem_flags参数指定一组标志。它低端的9个比特是该信号量的权限,其格式和含义
    都与系统调用open的mode参数相同。此外,它还可以和IPC_CREAT标志做按位“或”运算以创建新的信号量集。此时即使信号量已经存在,semget也不会产生错误。我们还可以联合使用IPC_CREAT 和IPC_EXCL标志来确保创建一组新的、唯一的信号量集。在这种情况下,如果信号量集已经存在,则semget返回错误并设置errno 为EEXIST。这种创建信号量的行为与用O_CREAT和O_EXCL标志调用open来排他式地打开一个文件相似。

    如果semget用于创建信号量集,则与之关联的内核数据结构体semid_ds将被创建并初始化。semid ds结构体的定义如下.

    struct ipc_perm
    {
    key_t key;
    uid_t uid;
    gid_t gid;
    uid_t cuid;
    gid_t cgid;
    mode_t mode;
    
    }
    struct semid_ds
    {
    struct ipc_perm sem_perm;
    unsigned long int sem_nsems;//树木
    time_t sem_otime;//最后一次调用semop
    time_t sem_ctime;//最后一次调用semctl
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    semget对semid_ds结构体的初始化:

    将sem perm.cuid和sem_perm.uid设置为调用进程的有效用户ID
    将sem_perm.cgid和sem_perm.gid设置为调用进程的有效组ID。
    将sem _perm.mode的最低9位设置为sem_flags参数的最低9位。
    将sem_nsems 设置为num_sems.
    将sem_otime设置为0。
    将sem_ctime设置为当前的系统时间。

    semop

    semop系统调用改变信号量的值,介绍与每个信号量关联的一些重要的内核变量:

    unsigned short semval //信号量的值
    unsigned short semzcnt //等待信号量的值变成0
    unsignedd short semzcnt //等待信号量增加的量
    pit_t sempid //最后一次执行semop的进程ID

    scmop对信号量的操作实际上就是对这些内核变量的操作。scmop的定义如下:

    #include
    int sem(int sem_di,struct sembuf* sem_ops,size_t num_sem_ops)
    //sembuf 是一个数组
    struct sembuf
    {
    unsigned short int sem_num;
    short int sem_op;
    short int sem_flg;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中,sem_num成员是信号量集中信号量的编号,0表示信号量集中的第一个信号量。sem_op成员指定操作类型,其可选值为正整数、0和负整数。每种类型的操作的行为又受到sem_fig成员的影响。sem_flg的可选值是IPC_NOWAITSEM_UNDO。
    IPC_NOWAIT的含义是,无论信号量操作是否成功,semop调用都将立即返回,这类似于非阻塞IO操作。
    SEM_UNDO的含义是,当进程退出时取消正在进行的semop操作。具体来说,sem_op和sem_flg将按照如下方式来影响

    semop的行为:
    如果sem_op大于0,则semop将被操作的信号量的值semval增加 sem_op。该操作要求调用进程对被操作信号量集拥有写权限。此时若设置了SEM_UNDO标志,则系统将更新进程的semadj变量(用以跟踪进程对信号量的修改情况)。

    如果sem_op等于0,则表示这是一个“等待0”( wait-for-zero)操作。该操作要求调用进程对被操作信号量集拥有读权限。如果此时信号量的值是0,则调用立即成功返回。如果信号量的值不是0,则semop 失败返回或者阻塞进程以等待信号量变为0。在这种情况下,当IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置errno为EAGAIN。如果未指定IPC_NOWAIT标志,则信号量的semzcnt值加1,进程被投入睡眠直到下列3个条件之一发生:信号量的值semval变为0,此时系统将该信号量的semzcnt值减1﹔被操作信号量所在的信号量集被进程移除,此时semop调用失败返回,errno被设置为EIDRM;调用被信号中断,此时semop调用失败返回,errno被设置为EINTR,同时系统将该信号量的semzcnt值减1。

    如果sem_op小于0,则表示对信号量值进行减操作,即期望获得信号量。该操作要求调用进程对被操作信号量集拥有写权限。如果信号量的值semval大于或等于sem_op 的绝对值,则semop操作成功,调用进程立即获得信号量,并且系统将该信号量的semval值减去sem_op的绝对值。此时如果设置了SEM_UNDO标志,则系统将更新进程的semadj变量。如果信号量的值semval小于sem_op的绝对值,则semop失败返回或者阻塞进程以等待信号量可用。在这种情况下,当IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置errno为EAGAIN。如果未指定IPC_NOWAIT标志,则信号量的semncnt值加1,进程被投入睡眠直到下列3个条件之一发生:信号量的值semval变得大于或等于sem_op 的绝对值,此时系统将该信号量的semncnt值减1,并将semval减去sem_op 的绝对值,同时,如果SEM_UNDO标志被设置,则系统更新semadj变量﹔被操作信号量所在的信号量集被进程移除,此时semop调用失败返回,errno被设置为EIDRM ;调用被信号中断,此时semop 调用失败返回,errno被设置为EINTR,同时系统将该信号量的semncnt值减1。

    semctl

    #include
    int sem(int sem_id,int sem_num,int command)
    
    • 1
    • 2

    sem_id参数是由semget调用返回的信号量集标识符,用以指定被操作的信号量集。sem_num参数指定被操作的信号量在信号量集中的编号。command参数指定要执行的命令。有的命令需要调用者传递第4个参数。第4个参数的类型由用户自己定义,但sys/sem.h头文件给出了它的推荐格式,具体如下:

         union semun {
                   int              val;    /* Value for SETVAL */
                   struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
                   unsigned short  *array;  /* Array for GETALL, SETALL */
                   struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                               (Linux-specific) */
               };
    
    
    
      struct  seminfo {
                             int semmap;  /* Number of entries in semaphore 
                                             map; unused within kernel  */
                             int semmni;  /* Maximum number of semaphore sets */
                             int semmns;  /* Maximum number of semaphores in all
                                             semaphore sets */
                             int semmnu;  /* System-wide maximum number of undo
                                             structures; unused within kernel */
                             int semmsl;  /* Maximum number of semaphores in a
                                             set */
                             int semopm;  /* Maximum number of operations for
                                             semop(2) */
                             int semume;  /* Maximum number of undo entries per
                                             process; unused within kernel */
                             int semusz;  /* Size of struct sem_undo */
                             int semvmx;  /* Maximum semaphore value */
                             int semaem;  /* Max. value that can be recorded for
                                             semaphore adjustment (SEM_UNDO) */
                         };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    semctl的command参数
    在这里插入图片描述
    注意:这些操作中,GETNCNT、GETPID、GETVAL、GETZCNT和SETVAL操作的是单个信号量,它是由标识符sem_id指定的信号量集中的第sem_num个信号量﹔而其他操作针对的是整个信号量集,此时semctl的参数sem num被忽略。

    内存共享

    共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输。这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。因此,共享内存通常和其他进程间通信方式一起使用。

    shmget shmat shmdt 和 shmctl

    #include
    int semget(key_t size_t size,int shmflg);

    和semget系统调用一样,key参数是一个键值,用来标识一段全局唯一的共享内存。size参数指定共享内存的大小,单位是字节。如果是创建新的共享内存,则size值必须被指定。如果是获取已经存在的共享内存,则可以把size设置为0。

    shmflg参数的使用和含义与semget系统调用的sem_flags参数相同。不过 shmget支持两个额外的标志———SHM_HUGETLB和SHM_NORESERVE。它们的含义如下:

    SHM_HUGETLB,类似于mmap的MAP_HUGETLB标志,系统将使用“大页面”来为共享内存分配空间。
    SHM_NORESERVE,类似于mmap的MAP_NORESERVE标志,不为共享内存保留交换分区(swap空间)。这样,当物理内存不足的时候,对该共享内存执行写操作将触发SIGSEGV信号。

       struct shmid_ds {
                   struct ipc_perm shm_perm;    /* Ownership and permissions */
                   size_t          shm_segsz;   /* Size of segment (bytes) */
                   time_t          shm_atime;   /* Last attach time */
                   time_t          shm_dtime;   /* Last detach time */
                   time_t          shm_ctime;   /* Last change time */
                   pid_t           shm_cpid;    /* PID of creator */
                   pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
                   shmatt_t        shm_nattch;  /* No. of current attaches */
                   ...
               };
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    将shm_perm.cuid和shm_perm.uid设置为调用进程的有效用户ID.
    将shm_perm.cgid和 shm_perm.gid设置为调用进程的有效组ID。
    将shm_perm.mode的最低9位设置为shmflg参数的最低9位。将shm_segsz设置为size。
    将shm_lpid、 shm_nattach、shm_atime、shm_dtime设置为0。
    将shm_ctime设置为当前的时间。

    共享内存被创建/获取之后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中。使用完共享内存之后,我们也需要将它从进程地址空间中分离。这两项任务分别由如下两个系统调用实现:
    #include
    void* shmat(int shm_id,const void* shm_addr,int shmflg)
    int shmdt(const void* shm_addr);

    shmtl系统调用:
    shmctl(int shm_id,int command,struct shmid_ds* buf)

    在这里插入图片描述

  • 相关阅读:
    OpenCV数字图像处理实战一:去水印(C++)
    DTCloud 复杂字段类型
    python中的bisect模块与二分查找
    多线程&并发篇---第四篇
    【计算机视觉】24-Object Detection
    JavaScript codePointAt() 方法
    Java 插入公式到PPT幻灯片
    WEB安全之PHP基础(九):正则表达式
    说一下vue响应式原理?可不只有proxy
    基于Java Swing和BouncyCastle的证书生成工具
  • 原文地址:https://blog.csdn.net/qq_62309585/article/details/126672991