• Linux 高性能服务器编程——多进程编程


    问题聚焦:
        进程是Linux操作系统环境的基础。
        本篇讨论以下几个内容,同时也是面试经常被问到的一些问题:
        1 复制进程映像的fork系统调用和替换进程映像的exec系列系统调用
        2 僵尸进程
        3 进程间通信的方式之一:管道
        4 3种System V进程通信方式:信号量,消息队列和共享内存




    fork系统调用

    定义:
    #include <sys/types.h>
    #include <unistd.h>
    pid_t fork( void );
    函数说明:
    • 该函数每次调用返回两次。
    • 在父进程中返回的是子进程的PID,在子进程中返回的是0,由此判断当前进程是父进程还是子进程(返回值是0的为子进程)。
    作用:
    • fork函数复制当前进程,在内核进程表中创建一个新的进程表项。
    • 子进程的许多属性被赋予了新的值,比如该进程的PPID 被设置成原进程的PID ,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。
    • 子进程的代码和父进程完全相同。
    • 子进程复制父进程的大部分数据(堆数据,栈数据,静态数据)。
    • 写时复制。即只有再任一进程(父进程或者子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。
    • 父进程中打开的文件描述符等在子进程中默认打开,因此它们的引用变量均+1。


    Exec系列系统调用(6个)

    在子进程中执行其他程序,即替换当前进程映像,需要使用exec系列函数:
    定义:
    #include <unistd.h>
    extern char** environ;
    
    int execl( const char* path, const char* arg, ... );
    int execlp( const char* file, const char* arg, ... );
    int execle( cost 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 execve( const char* path, char* const envp[] );
    参数说明:
    path:指定可执行文件的完整路径
    file:接受文件名,该文件的具体位置则在环境变量PATH中搜寻
    arg:接收可变参数
    argv:接收参数数组,它们都会被传递给新程序的main函数
    envp:用于设置新程序的环境变量,如果未设置,则新程序使用由全局变量environ指定的环境变量

    返回:
    成功时,不返回;出错,返回-1,并设置errno
    如果没出错,则源程序中exec调用之后的代码都不会执行,因为此时源程序已经被exec的参数指定的程序完全替换(包括代码和数据)

    exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置了类型SOCK_CLOEXEC的属性。




    僵尸进程

           对于多进程程序而言,父进程一般需要跟踪子进程的退出状态。因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)。

    僵尸进程:两种情况导致子进程处于僵尸态
    • 在子进程结束之后,父进程读取其退出状态之前,该子进程处于僵尸态。
    • 如果父进程退出或异常终止,而子进程继续运行,此时子进程的PPID(父进程PID)被设置成1,即init进程,即init进程接管了该进程,并等待它结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸态。
    即:父进程没有正确处理子进程的返回信息,将导致子进程处于僵尸态,占据内核资源,造成内核资源的浪费。

    下面介绍的函数在父进程中调用,以等待子进程的结束,并获得子进程的返回信息,从而避免了僵尸进程的产生,或者使得子进程的僵尸态立即结束:
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait( int* stat_loc );
    pid_t waitpid( pid_t pid, int* stat_loc, int options );

    函数说明:
    wait函数:阻塞该进程,直到该进程的某个子进程结束运行为止。返回子进程的PID,并将该子进程的退出状态信息存储于stat_loc参数指向的内存中。
    sys/wait.h头文件中定义了几个宏,解释子进程的退出状态信息


    waitpid函数:等待由pid参数指定的子进程。
           当options参数设置为WHOHANG时,waitpid调用是非阻塞的:如果pid指定的目标子进程还没结束或意外终止,则waitpid立即返回0;如果目标子进程确实正常退出了,则waipid返回该子进程的PID。调用失败返回-1,并设置errno。
           waitpid只等待由pid参数指定的子进程。如果pid取值为-1,那么和wait函数相同。

    Demo: 要在事件已经发生的情况下执行非阻塞调用才能提高程序的效率
    使用waitpid函数实现这一思想:
    static void handle_child( int sig )
    {
        pid_t pid;
        int stat;
        while ( ( pid = waitpid( -1, &stat, WHOHANG )) > 0 )
        {
            /* 对结束的子进程进行善后处理 */
        }
    }
    看不太懂?貌似我们错过了某些重要的东西,主要是因为中间跳过了两章,直接看我感兴趣的多进程。。
    我们先了解一下,当一个进程结束时,它将给其父进程发送一个SIGCHLD信号,因此,我们可以在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以“彻底结束”一个子进程。



    管道pipe

    作用:父进程和子进程间通信的常用手段,半双工模式
    方式:fork系统调用后两个管道文件描述符(fd[0]和fd[1]都保持打开),通信时,父进程和子进程必须一个关闭fd[0],另一个关闭fd[1]。
    注意:socket编程接口提供了一个创建全双工管道的系统调用:socketpair
                                 
    管道只能用于有关联的两个进程间的通信。




    下面介绍3种System V IPC进程间通信方式


    信号量

    背景:当多个进程同时访问系统上的某个资源的时候,就需要考虑同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。
    临界区:对资源访问的关键代码,可能引发竞态条件
    信号量:确保临界区代码的独占式访问。
    • 一种特殊的变量
    • 只能取自然数值
    • 只支持两种操作:等待(P,要进入临界区)和信号(V,要退出临界区)
    含义:假设有信号量SV,对它的P、V操作含义如下
    • P(SV),如果SV的值大于0,就将它减一,然后允许访问临界区;如果SV的值为0,则挂起进程的执行。
    • V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加一
           当信号量的值为1时,就是退化成互斥锁。如下图所示:
                                      
           进程A和进程B在执行关键代码段(临界区)的前后,都会对SV进行PV操作,以确保临界区的独占式访问。


    Linux信号量的API,三个系统调用:semget、semop和semctl,用于操作信号量集 。所以看起来会比简单的互斥锁要复杂一点。

    semget系统调用
    作用:创建一个新的信号量集,或者获取一个已经存在的信号量集
    定义:
    #include <sys/sem.h>
    int semget ( key_t key, int num_sems, int sem_flags );
    参数说明:
    key:一个键值,标识一个全局唯一的信号量集,要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量。
    num_sums:指定要创建/获取的信号量集中信号量的数据。如果是创建信号量,则该值必须被指定;如果是获取已经存在的信号量,则可以把它设置为0。
    sem_flags:指定一组标志。它低端的9个比特是该信号量的权限,其格式和含义都与系统调用open的mode参数相同。
    返回:成功时,返回一个正整数值,它是信号量集的标识符;semget失败时返回-1,并设置errno。

    semop系统调用
    作用:改变信号量的值,即执行P、V操作。
    定义:
    #include <sys/sem.h>
    int semop ( int sem_id, struct sembuf* sem_ops, size_t num_sem_ops );
    参数说明:
    sem_id:由semget调用返回的信号量集标识符,用以指定被操作的目标信号量集。
    sem_ops:指向一个sembuf结构体类型的数组。
    struct sembuf:
    struct sembuf
    {
        unsigned short int sem_num;    // 信号量集中信号量的编号
        short int sem_op;                        // 指定操作类型,可选正整数,0,负整数
        short int sem_flg;                        // 影响操作行为,可选值IPC_NOWAIT(无论信号量操作是否成功,都立即返回),SEM_UNDO(当进程退出时,取消正在进行的semop操作)
    };

    num_sem_ops:指定要执行的操作个数,即sem_ops数组中元素的个数。semop对数组中的每个成员按数组顺序依次执行,该过程是原子操作。

    semctl系统调用
    作用:允许调用者对信号量进行直接控制
    定义:
    #include <sys/sem.h>
    int semctl ( int sem_id, int sem_num, int command, ... );
    参数说明:
    sem_id:由semget调用返回的信号量标识符。
    sem_num:指定被操作的信号量在信号量集中的编号。
    command:指定要执行的命令。
    第4个参数由用户自定义,在sys/sem.h头文件中给出了它的推荐格式。
    union semun
    {
        int val;                                        //用于SETVAL命令
        struct semid_ds *buf;              //用于IPC_STAT和IPC_SET命令
        unsigned short *array;            //用于GETALL和SETALL命令
        struct seminfo *__buf;             //用于IPC_INFO命令
    };
    semctl支持的所有命令如图:
               
          sem_ctl成功时的返回值取决于command参数,失败时返回-1并设置errno。

    特殊键值IPC_PRIVATE
    semget的key参数的特殊值,其值为0。
    作用:
           这样无论该信号量是否存在,semget都将创建一个新的信号量。使用该键值创建的信号量并非像它的名字那样是进程私有的,其他进程,尤其是子进程,也有方法来访问这个信号量。

    代码:使用IPC_PRIVATE创建信号量
    #include <sys/sem.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    union semun
    {
        int val;
        struct semid_ds *buf;
        unsigned short int *array;
        struct seminfo *__buf;
    };
    
    //op为-1时执行P操作, op为1时执行V操作
    void pv(int sem_id, int op)
    {
        struct sembuf sem_b;
        sem_b.sem_num = 0;
        sem_b.sem_op = op;
        sem_b.sem_flg = SEM_UNDO;
        
        semop(sem_id, &sem_b, 1);
    }
    
    int main(int argc, char *argv[])
    {
        int sem_id = semget(IPC_PRIVATE, 1, 0666);
    
        union semun sem_un;
        sem_un.val = 1;
    
        semctl(sem_id, 0, SETVAL, sem_un);
    
        pid_t id = fork();
    
        if (id < 0) {
            fprintf(stderr, "fork failed.
    ");
            return 1;
        }
        else if (id == 0) {
            printf("child try to get binary sem
    ");
    
            pv(sem_id, -1);
            printf("child get the sem and would release it after 5 seconds
    ");
            sleep(5);
            pv(sem_id, 1);
            
            exit(0);
        }
        else{
            printf("parent try to get binary sem
    ");
            pv(sem_id, -1);
            printf("parent get the sem and would release it after 3 seconds
    ");
            sleep(3);
            pv(sem_id, 1);
        }
    
        waitpid(id, NULL, 0);
        
        //删除信号量
        semctl(sem_id, 0, IPC_RMID, sem_un);
    
    
        return 0;
    }



    共享内存
    特点:
    • 不涉及进程之间的任何数据传输。
    • 必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。 
    • 因此,共享内存通常和其他进程间通信方式一起使用
    Linux共享内存的API都定义在sys/shm.h,包括4个系统调用:shmget、shmat、shmdt、shmctl。

    shmget系统调用
    作用:创建一段新的共享内存,或者获取一段已经存在的共享内存。
    定义:
    #include <sys/shm.h>
    int shmget ( key_t key, size_t size, int shmflg );
    参数说明:
    key:标识一段全局唯一的共享内存。
    size:指定共享内存的大小,单位是字节。
    shmflg:和semget系统调用的sem_flags参数相同,支持两个额外的标志:
    • SHM_HUGETLB:类似于mmap的MAP_HUGETLB标志,系统将使用“大页面”来为共享内存分配空间。
    • SHM_NORESERVE:类似于mmap的MAP_NORESERVE标志,不为共享内存保留交换分区(swap空间)。这样,当物理内存不足的时候,对该共享内存执行写的操作将触发SIGSEGV信号。
           如果shmget 用于创建共享内存,则这段共享内存的所有直接都被初始化为0。

    shmat和shmdt系统调用
    使用共享内存需要注意的两个任务:
    • 共享内存在创建之后,不能立即使用,需要先将它关联到进程的地址空间中。——shmat
    • 使用完共享内存后,我们也需要把它从进程地址空间中分离。——shmdt
    定义:
    #include <sys/shm.h>
    void shmat ( int shm_id, const void * shm_addr, int shmflg );
    int shmdt ( const void* shm_addr );
    参数说明:
    shm_id:共享内存标识符。
    shm_addr:指定要将共享内存关联到进程的哪块地址空间,最终的效果还受到shmflg参数的可选标志SHM_RND的影响:
    • 如果shm_addr为NULL,则被关联的地址由操作系统选择。
    • 如果shm_addr非空,并且SHM_RND标志未被设置,则共享内存被关联到addr指定的地址处。
    • 如果shm_addr非空,并且SHM_RND标志被设置,则被关联的地址是[ shm_addr -(shm_addr % SHMLBA) ]。SHMLBA的含义是“段低端边界地址倍数”,它必须是内存页面大小的整数倍。现在的Linux 内核中,它等于一个内存页大小。SHM_RND 的含义是圆整,即将共享内存被关联的地址向下圆整到离shm_addr最近的SHMLBA 的整数倍地址处。
    除了SHM_RND 标志外,shmflg参数还支持如下标志:
    • SHM_RDONLY:进程仅能读取共享内存中的内容。若没有指定该标志,则进程可同时共享内存进行读写操作(当然,这需要在创建共享内存的时候指定其读写权限)。
    • SHM_REMAP:如果地址shm_add已经被关联到一段共享内存上,则重新关联。
    • SHM_EXEC:它指定对共享内存段的执行权限。对共享内存而言,执行权限实际上和读权限是一样的。

    shmctl系统调用
    作用:控制共享内存的某些属性
    #include <sys/shm.h>
    int shmctl ( int shm_id , int command, struct shmid_ds* buf );
    参数说明:
    shm_id:共享内存标识符
    command:要执行的命令。支持的命令如下图所示:
                    
        
    无关进程之间共享内存的方式
    mmap可以实现无关进程之间的内存共享,需要文件支持。
    shm_open无需文件支持。
    定义:
    #include <sys/mman.h>
    #include <sys/stat.h>
    #include <fnctl.h>
    int shm_open ( const char* name, int oflag, mode_t mode );
    参数说明:
    name:指定要创建/打开的共享内存对象.
    oflag:指定创建方式:
    • O_RDONLY:以只读方式打开共享内存对象。
    • O_RDWR:以可读、可写方式打开共享内存对象。
    • O_CREAT:如果共享内存不存在,则创建之。此时mode参数的最低9位将指定该共享内存对象的访问权限。共享内存对象被创建的时候,其初始长度为0.
    • O_EXCL:和O_CREAT一起使用,如果由name指定的共享内存对象已经存在,则shm_open调用返回错误,否则就创建一个新的共享内存对象。
    • O_TRUNC:如果共享内存已经存在,则把它截断,使其长度为0.
           shm_open调用成功时返回一个文件描述符。该文件描述符可用于后续的mmap调用,从而将共享内存关联到调用进程。shm_open失败时返回-1,并设置errno。

    #include <sys/mman.h>
    #include <sys/stat.h>
    #include <fnctl.h>
    int shm_unlink ( const char *name );
    作用:将name参数指定的共享内存对象标记为等待删除。当所有使用该共享内存对象的进程都使用ummap将它从进程中分离之后,系统将销毁这个共享内存对象所占据的资源。
    注意:如果代码中使用了上述POSIX共享内存的函数,则编译的时候需要指定链接选项-lrt。



    消息队列

    作用:两个进程之间传递二进制块数据的一种方式,简单有效。
    特点:每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,不一定像管道和命名管道那样必须先进先出的方式接收数据。
    相关的API定义在sys/msg.h中,包括四个系统调用:msgget、msgsnd、msgrcv、msgctl

    msgget系统调用
    作用:创建一个消息队列,或者获取一个已有的消息队列
    定义:
    #include <sys/msg.h>
    int msgget ( key_t key, int msgflg );

    参数说明:
    key:标识一个全局唯一的消息队列。
    msgflg:和semget系统调用的sem_flags参数相同
    与内核数据结构msqid_ds相关联。

    msgsnd系统调用:
    作用:把一条消息添加到消息队列中。
    定义:
    #include <sys/msg.h>;
    int msgsnd ( int msgid, const void* msg_ptr, size_t msg_sz, int msgflg );
    • msgid:由msgget函数返回的消息队列标识符。
    • msg_ptr:指向一个准备发送的消息,消息必须被定义为如下的类型:
    struct msgbuf
    {
    	long mtype; // 消息类型
    	char mtext[512]; // 消息数据
    };
    • msgflg:控制msgsnd的行为。它通常仅支持IPC_NOWAIT标志,即以非阻塞的方式发送消息。默认情况下,发送消息时如果消息队列满了,则msgsnd函数将阻塞。若IPC_NOWAIT标志被指定,则msgsnd将立即返回并设置errno 为EAGAIN。
    处于阻塞状态的msgsnd调用可能被如下两种异常情况所中断:
    • 消息队列被移除。此时msgsnd调用将立即返回并设置errno为EIDRM。
    • 程序接收到信号。此时msgsnd调用将立即返回并设置errno为EINTR。


    msgrcv系统调用
    作用:从消息队列中获取消息。
    定义:
    #include <sys/msg.h>
    int msgrcv ( int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg );
    • msqid:由msgget调用返回的消息队列标识符。
    • msg_ptr:用于存储接收的消息。
    • msg_sz:指的是消息数据部分的长度。
    • msgtype:指定接收何种类型的消息,可以如下几种方式:(1) msgtype 等于0 。读取消息队列中的第一个消息。 (2)msgtype大于0 。读取消息队列中第一个类型为msgtype的消息。 (3)msgtype 小于0 。读取消息队列中第一个类型值比msgtype的绝对值小的消息。
    • msgflg:控制msgrcv 函数的行为。它可以是如下一些标志的按位或:(1)IPC_NOWAIT,如果消息队列中没有任何消息,则msgrcv调用立即返回并设置errno为ENOMSG。(2)MSG_EXCEPT,如果msgtype大于0,则接收消息队列中第一个非msgtype 类型的消息。 (3)MSG_NOERROR,如果消息数据部分的长度超过了msg_sz,就将它截断。
    处于阻塞状态的msgrcv调用还可能被如下两种异常情况所中断:
    • 消息队列被移除。此时msgsnd调用将立即返回并设置errno为EIDRM
    • 程序接收到信号此时msgsnd调用将立即返回并设置errno为EINTR



    msgctl系统调用
    作用:控制消息队列的某些属性。
    定义:
    #include <sys/msg.h>
    int msgctl ( int msqid, int command, struct msqid_ds* buf );

    command 参数指定执行的命令:


    msgctl成功时返回值取决于command参数,失败时就返回-1,并设置errno。



    小结:
    • 三种System V IPC进程间通信方式都使用一个全局唯一的键值来描述一个共享资源。使用ipcs命令可以查看当前系统的共享资源实例。
    • 要尽可能缩短僵尸进程的存在时间
    • 简单了解一下fork和exec系系统调用

    参考资料:
    《Linux高性能服务器编程》
  • 相关阅读:
    配置Express中间件
    C#字符串中特殊字符的转义
    JSON.NET 简单的使用
    ASP.NET 解决URL中文乱码的解决
    ASP.NET MVC 笔记
    VS中一些不常用的快捷键
    Visual Studio 中突出显示的引用
    Silverlight从客户端上传文件到服务器
    silverlight打开和保存文件
    sliverlight资源文件的URI调用
  • 原文地址:https://www.cnblogs.com/wangfengju/p/6172414.html
Copyright © 2020-2023  润新知