美文网首页Linux我爱编程C++多线程
Linux系统编程(二) ------ 多进程编程

Linux系统编程(二) ------ 多进程编程

作者: 穹蓝奥义 | 来源:发表于2017-01-08 21:21 被阅读451次

    一、进程的创建和调度

    相关概念:
    • 最基础的计算机动作被称为指令(instruction)。
    • 程序(program),就是一系列指令的所构成的集合。通过程序,我们可以让计算机完成复杂的操作。程序大多数时候被存储为可执行的文件。

    1.进程

    进程(process)是操作系统的概念,为程序的一次执行在操作系统中或内存中的映像,同时伴随着资源的分配和释放。并具有以下特征的活动单元。

    • 一组执行的指令序列
    • 一个当前状态
    • 相关的系统资源集合

    提示: 进程是程序的一个具体实现,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,进程是执行程序的过程,同一个程序可以执行多次,每次都可以在内存中开辟独立的空间来装载,从而产生多个进程。不同的进程还可以拥有各自独立的IO接口。操作系统的一个重要功能就是为进程提供方便,比如说为进程分配内存空间,管理进程的相关信息等等。

    进程和程序的区别:
    程序是态的,它是一些保存在磁盘上得指令的有序集合,没有任何执行的概念。
    进程是一个动态的概念,它是程序执行的过程,包括创建、调度和消亡。

    2.进程控制块

    进程在操作系统中都有一个户口,用于表示这个进程。这个户口操作系统被称为进程控制块PCB(process control block),在Linux中具体实现是 task_struct数据结构,它记录了一下几个类型的信息:

    • 进程描述信息:
      进程标识符用于唯一的标识一个进程(pid,ppid)。
    • 进程控制信息:
      进程当前状态 //如这个进程处于可执行状态,休眠,挂起等。
      进程优先级
      程序开始地址
      各种计时信息
      通信信息
    • 资源信息:
      占用内存大小及管理用数据结构指针
      交换区相关信息
      I/O设备号、缓冲、设备相关的数结构
      文件系统相关指针
      资源的限制和权限
    • 现场保护信息(cpu进行进程切换时):
      寄存器
      PC
      程序状态字PSW
      栈指针

    对于操作系统来说PCB即找到整个过程。

    3.进程主要管理执行所需的资源

    执行单元由线程管理。

    4.进程id

    inux内核通过唯一的进程标识符PID来标识每个进程。PID存放进程描述符的pid字段中,新创建的PID通常是前一个进程的PID加1,不过PID的值有上限。当系统启动后,内核通常作为一个进程的代表。一个指向task_struct的宏current用来记录正在运行的进程。current经常作为进程描述符结构指针的形式出现在内核代码中,例如,current->pid表示处理器正在执行进程的PID。

    使用函数取得相关的进程识别码
    • 手册文件 man getpid / man getuid / man getgid

    函数头文件及函数原型
    #include <sys/types.h>
    #include <unistd.h>
    pid_t getpid(void) ; //获取目前调用进程的进程ID
    pid_t getppid(void) ; //获取目前调用进程的父进程ID
    uid_t getuid(void) ; //获取目前调用进程的实际用户ID
    gid_t getgid(void) ; //获取目前调用进程的实际组ID

    函数参数:
    函数说明及返回值:许多程序利用取到的id来建立临时文件, 以避免临时文件相同带来的问题。

    5.使用fork()函数复制创建新进程

    • 手册文件 man fork

    函数头文件及函数原型
    #include <unistd.h>
    pid_t fork(void);

    函数参数:
    函数说明及返回值:在Linux中创建一个新进程的唯一方法是使用fork()函数。fork()函数是Linux中一个非常重要的函数,用于从已存在的进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程的上下文、代码段、进程堆栈、内存信息、打开的文件描述符、符号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、信号处理方式和控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。
    实际是在父进程中执行fork()函数时,父进程会复制一个子进程,而且父子进程的代码从fork()函数的返回开始分别在两个地址空间中同时运行,从而使两个进程分别获得所属fork()函数的返回值,其中在父进程中的返回值是子进程的进程号,而在子进程中返回0,若出错返回-1。

    提示:

    父进程子进程间的前后顺序,要看系统进程调度策略。同时可以看出,使用fork()函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得fork()函数的系统开销比较大,而且执行速度也不是很快。为了加快fork()的执行速度,很多UNIX系统设计者创建了vfork()。vfork()也能创建新的进程,但它不产生父进程的副本。它是通过允许父子进程可访问相同物理内存,从而伪装了对进程地址空间的真实复制,当子进程需要改变内存中的数据时才复制父进程。这既是著名的“写操作时复制”(copy-on-write)技术。现在大部分嵌入式Linux系统的fork()函数调用已经采用vfork()函数的实现方式,例如uCLinux所有的多进程管理都通过vfork()来实现。

    Linux下fork函数及pthread函数的总结

    fork()后一定exec()替换

    6.使用exec()函数族替换成为新进程

    • 手册文件 man exec / man execve

    函数头文件,函数原型及参数说明

          #include <unistd.h>
          extern char **environ;
          int execl(const char *path, const char *arg, ...);
          int execv(const char *path, char *const argv[]);
          int execle(const char *path, const char *arg, ..., char *const envp[]);
          int execlp(const char *file, const char *arg, ...);
          int execvp(const char *file, char *const argv[]);
          int execve(const char *filename, char *const argv[], char *const envp[]);
          int execvpe(const char *file, char *const argv[],char *const envp[]);
    

    函数参数:

    第一个参数为查找方式。前面3个函数的查找方式都是完整的文件目录路径,
    而后4个函数(也就是带p的函数)可以只给出文件名,系统就会自动安装环境变量"$PATH"所指定的路径进行查找。
    第二个参数为传递方式。exec函数族的参数传递有两种方式:一种是逐个列举方式,
    而另一种则是将所有参数整体构造指针数组传递。在这里是以函数名的第5个字母来区别的,
    - 带"**l**"(list)的表示逐个列举参数的方式,
    其语法为const char *arg;
    - 带“**v**”(vetor)的表示将所有参数整体构造指针数组传递,
    其语法为char *const argv[]。
    参数实际上就是用户在使用这个可执行文件时所需要的全部命令选项字符串(包括该可执行程序命令本身)。
    要注意的是,这些参数必须以NULL结束。
    第三个参数为环境变量。exec函数族可以默认系统的环境变量,也可以传入指定的环境变量。
    这里以“e”(environment)结尾的两个函数execle()和execve()
    就可以在envp[]中指定当前进程所使用的环境变量。
    

    函数返回值:如果执行成功则函数不会返回, 执行失败则直接返回-1。

    提示:

    事实上,这6个函数中真正的系统调用只有execve(),其他5个都是库函数,它们最终都会调用execve()这个系统调用。在使用exec函数族是,一定要加上错误判断语句。

    7.使用:wait()/waitpid() 暂时停止目前进程的执行, 直到有信号来到或子进程结束。

    • 手册文件 man wait

    函数头文件及函数原型
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *status);
    pid_t waitpid(pid_t pid, int *status, int options);

    函数说明、参数及返回值:
    wait()函数用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或该进程接收到一个指定的信号为止。如果该父进程没有子进程或它的子进程已经结束,则wait()就会立即返回。waitpid()的作用和wait()一样,但它并不一定要等待一个终止的子进程,它还有若干选项,如可提供一个非阻塞版本的wait()功能,也能支持作用控制。实际上,wait()函数只是waitpid()函数的一个特例,在Linux内部实现wait()函数时直接调用的就是waitpid()函数。

    status 是一个整型指针,是该子程序退出的状态。```
    如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值。子进程的结束状态值会由不为空的参数status 返回, 而子进程的进程识别码也会一快返回。如果不在意结束状态值, 则参数status 可以设成NULL。
    ```c
    参数pid 为欲等待的子进程识别码, 其他数值意义如下:
    1、pid<-1 等待进程组识别码为pid 绝对值的任何子进程。
    2、pid=-1 等待任何子进程, 相当于wait()。
    3、pid=0 等待进程组识别码与目前进程相同的任何子进程。
    4、pid>0 只等待任何子进程识别码等于pid 的子进程,不管是否有其他子进程结束,
             只要指定子进程未结束,一直等。```
    
    参数option 可以为0 或下面的OR 组合:
    ```c
    WNOHANG:  如果没有任何已经结束的子进程则马上返回, 不予以等待。
    WUNTRACED:如果子进程进入暂停执行情况则马上返回, 但结束状态不予以理会. 
               子进程的结束状态返回后存于status。
    0:        阻塞父进程,等待子进程退出。```
    
    底下有几个宏可判别结束情况:
    ```c
    WIFEXITED(status):  用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
    WEXITSTATUS(status):取得子进程exit()返回的结束代码, 
                         一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。
                         可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,
                         WEXITSTATUS(status)就会返回5;如果子进程调用exit(7)退出,
                         WEXITSTATUS(status)就会返回7。如果进程不是正常退出,
                         也就是说,WIFEXITED返回0,这个值就毫无意义了。
    WIFSIGNALED(status):如果子进程是因为信号而结束则此宏值为真。
    WTERMSIG(status):   取得子进程因信号而中止的信号代码, 
                         一般会先用WIFSIGNALED 来判断后才使用此宏。
    WIFSTOPPED(status): 如果子进程处于暂停执行情况则此宏值为真. 
                         一般只有使用WUNTRACED时才会有此情况。
    WSTOPSIG(status):   取得引发子进程暂停的信号代码, 
                        一般会先用WIFSTOPPED 来判断后才使用此宏。```
    
    **如果执行成功则返回已结束的子进程识别码(PID), 如果有错误发生则返回-1,使用选项WNOHANG且没有子进程退出则返回0。
    当status为NULL时,只要有子进程退出,wait()退出阻塞(且返回值为退出的子进程的进程号),否则一直阻塞直到有子进程退出。当调用wait()函数的进程没有子进程时,返回-1。**
    ######提示:
    如果参数status的值不是NULL,wait()就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是非正常结束的(一个进程也可以被其他进程用信号结束),以及正常结束时返回值,或被哪一个信号结束的等信息。
    
    
    ####8.进程的执行状态:新建 就绪 运行 阻塞 退出(睡眠)
    
    ![进程状态模型](http:https://img.haomeiwen.com/i3963687/14b236efa2b7e931.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    ####9.僵尸进程
    进程运行结束,父进程尚未使用wait()函数族(如使用waitpid()函数)等系统调用来“收尸”,即等待父进程销毁它。处于该状态下的进程“实体”已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能调度,仅仅在进程列表保留一个位置,记载该进程的退出状态等信息供其他进程收集,该子进程将会持续处于僵尸状态。僵尸进程将会导致资源浪费。
    ####10.孤儿进程
    父进程在子进程之前退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作,最终还是会死掉。
    
    ##二、信号
    信号signal处理是Linux程序的一个特色,用信号处理来模拟操作系统的中断功能,对于系统程序员来说是最好的一个选择了。
    **信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称软中断**。从它的命名可以看出,它的实质和使用很像中断,所有,信号可以说是进程控制的一部分。
    信号只是异步通知某进程发生了什么事件,并不给该进程传递任何数据。
    
    ####1.收到信号的进程对各种信号有不同的处理方法。
    处理方法可以分为三类:
    
        第一种是类似中断的处理程序,对于需要处理的信号,进程可以自定义信号处理函数。
        第二种是忽略某个信号,收到信号但对该信号不做任何处理,就像从未发生过一样。
        第三种是对该信号的处理保留系统的默认值,对大部分的信号的缺省操作是使得进程终止。
    
    常用信号及其处理:
    SIGINT/SIGQUIT/SIGALRM/SIGCHLD
    详情请见:[ Linux下C语言开发(信号signal处理机制)](http://blog.csdn.net/thanksgining/article/details/41824475)
    
    ####2.使用signal()函数注册设定某个信号的处理方法
    - 手册文件 man signal 
    >函数头文件及函数原型
    ```c
     #include <signal.h>
    void (*signal(int sig, void (*func)(int)))(int);
    或
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);```
    
    *函数说明及参数:*
    ```c
    表达式一:
    int (*p)();这是一个函数指针, p所指向的函数是一个不带任何参数, 且返回值为int的函数。
    int (*fun())();这个式子与上面式子的区别在于用fun()代替了p,而fun()是一个函数,
        所以说就可以看成是fun()这个函数执行之后,它的返回值是一个函数指针,
        此函数指针(暨上面的p)所指向的函数是一个不带任何参数,且返回值为int的某一函数。
    void (*signal(int signo, void (*handler)(int)))(int);
        就可以看成是signal()函数(它自己是带两个参数,一个为整型,一个为函数指针的函数),
        而这个signal()函数的返回值也为一个函数指针,这个函数指针指向一个带一个整型参数,
        并且返回值为void的一个函数。
        在写信号处理函数时对于信号处理的函数也是void sig_fun(int signo);
        恰好与上面signal()函数所返回的函数指针所指向的函数是一样的
        void ( *signal() )( int );
    表达式二:
    在调用中,signal()会依参数signum 指定的信号编号来设置该信号的处理函数。
    当指定的信号到达时就会跳转到参数handler 指定的函数执行。
    如果参数handler不是函数指针, 则必须是下列两个常数之一:
    1、SIG_IGN 忽略参数signum 指定的信号。
    2、SIG_DFL 将参数signum 指定的信号重设为核心预设的默认信号处理方式。```
    
    *函数返回值:* 返系统调用signal返回值的指定信号signum前一次的处理例程,暨信号处理函数指针, 如果有错误则返回SIG_ERR(-1)。
    ######提示:
    在信号发生跳转到自定的 handler 处理函数执行后, 系统会自动将此处理函数换回原来系统预设的处理方式, 如果要改变此操作请改用sigaction()。
    
    ####3.使用sigaction()检查或修改与指定信号相关联的处理动作(可同时两种操作)。
    - 手册文件 man sigaction 
    >函数头文件及函数原型
    ```c
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);```
    
    *函数说明及参数:*
    依参数signum指出要捕获的信号类型,暨指定设置新的信号编号来设置该信号的处理函数act,暨指定新的信号处理方式, 同时保留该信号原有的信号处理函数oldact,暨输出先前信号的处理方式(如果不为NULL的话)。
    
    参数signum 可以指定SIGKILL 和SIGSTOP 以外的所有信号。
    
    参数结构sigaction 定义如下:
    ``` c
    struct sigaction
    { 
       void (*sa_handler) (int); 
       sigset_t sa_mask;
       int sa_flags; 
       void (*sa_restorer) (void);
    }
    1、sa_handler 此参数和signal()的参数handler 相同, 代表新的信号处理函数。
    2、sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号搁置。
    3、sa_restorer 此参数没有使用。
    4、sa_flags用来设置信号处理的其他相关操作, 下列的数值可用:   
    SA_NOCLDSTOP: 如果参数signum 为SIGCHLD, 则当子进程暂停时并不会通知父进。  
    SA_RESETHAND: 当调用新的信号处理函数前, 将此信号处理方式改为系统预设的方式。   
    SA_RESTART:   被信号中断的系统调用会自行重启。  
    SA_NODEFER:   在处理此信号未结束前不理会此信号的再次到来。
    SA_INTERRUPT: 由此信号中断的系统调用不会自动重启。
    SA_SIGINFO: 提供附加信息,一指向siginfo结构的指针以及一指向进程上下文标识符的指针。
    
    如果参数oldact 不是NULL 指针,则原来的信号处理方式会由此结构sigaction返回。```
    
    *函数返回值:*执行成功则返回0, 如果有错误则返回-1。
    
    ######提示:
    sigaction()是POSIX的信号接口,而signal()是标准C的信号接口。
    
    发送信号方式:kill(实际是向指定的进程,发送信号)、组合键、硬件出故障。
    ####4.使用kill()函数向指定的进程发送一个信号
    
    - 手册文件 man 2 kill
    >函数头文件及函数原型
       ```c
       #include <sys/types.h>
       #include <signal.h>
       int kill(pid_t pid, int sig);```
    
    *函数说明及参数:*该系统调用可以用来向任何进程或进程组发送任何信号。参数sig 指定的信号给参数pid 指定的进程。参数pid 有几种情况:
    ```c
    1、pid>0 将信号传给进程识别码为pid 的进程。
    2、pid=0 将信号传给和目前进程相同进程组的所有进程。
    3、pid=-1 将信号广播传送给系统内所有的进程。
    4、pid<-1 将信号传给进程组识别码为pid 绝对值的所有进程参数 sig 代表的信号。
              即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。
    5.如果参数sig为0,将不发送信号。```
    
    *函数返回值:*执行成功则返回0, 如果有错误则返回-1。
    ######提示:
    一个进程被允许将信号发送到进程pid时,必须拥有root权限,或者是发出的进程的UID或EUID指定接收的进程的UID或保护用户ID(savedset-user-ID)相同。
    
    #######小提醒:
    数组传参,实际传的是,数组首元素的地址和长度
    typedef,实际是typerelay,重命名
    
    
    
    
    ##参考资料
    刘老师上课资料及网上前辈资料
    [Linux进程基础](http://www.cnblogs.com/vamei/archive/2012/09/20/2694466.html)
    [linux 进程(一)---基本概念](http://blog.chinaunix.net/uid-26833883-id-3193588.html)
    [Linux下C语言开发(多任务编程之任务、进程、线程)](http://blog.csdn.net/Thanksgining/article/details/41890293)
    [Linux下的进程控制块的数据结构](http://blog.csdn.net/yaoyepeng/article/details/5368516)
    [进程生命周期与进程控制块](http://www.cnblogs.com/mickole/p/3185889.html)
    [进程列表相关解释](http://blog.csdn.net/fenglibing/article/details/6958745)
    [UNIX/Linux-进程控制(实例入门篇) ](http://blog.csdn.net/yang_yulei/article/details/17404021)
    [浅谈Linux环境下并发编程中C语言fork()函数的使用](http://www.jb51.net/article/87166.htm)
    [linux中fork()函数详解(原创!!)](http://www.cnblogs.com/bastard/archive/2012/08/31/2664896.html)
    
    (或待继续整理...)

    相关文章

      网友评论

        本文标题:Linux系统编程(二) ------ 多进程编程

        本文链接:https://www.haomeiwen.com/subject/zugfbttx.html