Linux C多进程编程基础

关于进程概念相关的内容请打开链接,本文所介绍的是进程的POSIX标准。

进程的关系

       Linux中的所有进程都是相互联系的,进程之间的的从属关系有父/子关系和兄/弟关系。

       Linux内核创建了进程标号为0以及进程标号为1的进程。其中PID为1的进程是初始化进程init,Linux中的所有进程都是由其衍生而来的,在shell下执行程序启动的进程则是shell进程的子进程。进程可以再启动一个或多个进程,这样就形成了一颗进程树,每个进程都是树中的一个节点,其中树根就是初始化进程init。

       按进程的从属关系,进程分为以下几种(假设一个进程为P):

       ·祖先进程p_opptr(original parent):指向创建了进程P的进程的进程描述符,如果父进程不存在(例如已被销毁或从父进程中退出),则指向进程init的进程描述符。所以当一个shell用户启动了一个后台进程并从shell退出的时候,这个后台进程将变成init的子进程。

       ·父进程p_pptr(parent):指向创建了进程P的进程的进程描述符,其值通常来说和p_opptr一致,但也可能不同。

       ·子进程p_cptr(child):指向由进程P创建的进程中年龄最小(即创建时间最晚)的进程的进程描述符,即上一次创建的进程。

       ·兄进程p_osptr(older sibling):指向与P进程同属于一个父进程,但创建时间比P进程早的进程进程描述符。

       ·弟进程p_ysptr(younger sibling):与兄进程相对,指向与P进程同属于一个父进程,但创建时间比P进程较晚的进程。

进程的状态

       进程在其生存周期内可能处于以下状态中,且一个进程在同一时刻只能位于其中一个状态:

       ·可运行状态(TASK_RUNNING):占用处理器执行或准备执行。

       ·可中断的等待状态(TASK_INTERRUPTIBLE):进程被挂起或睡眠,当满足某些条件时才退出这种等待状态。这些条件包括:硬件中断、等待的资源被释放、传递一个信号灯,退出等待状态后会回到可运行态。

       ·不可中断的等待状态(TASK_UNINTERRUPTIBLE):和上一个状态相似,区别是当接收到信号时不能退出这个等待状态。

       ·暂停状态(TASK_STOPPING):进程的运行被暂停,通常来说是接收到SIGSTOP、SIGTTIN或者SIGTTOU信号后。如果一个进程被另外一个进程监控时,任何信号都可以把这个进程置于TASK_STOPPEN状态。

       ·僵尸状态(TASK_ZOMBIE):进程的执行已经被终止,但父进程还没有wait系列系统调用已返回的相应信息,此时内核不能丢弃与该进程有关的数据,因为父进程可能还需要这些数据。

       进程在这几种状态之间相互转化,但对于用户而言是透明的,这个切换的过程常被称为进程调度。进程是一个随执行过程不断变化的实体,和程序要包含指令和数据一样,进程也包含程序计数器和所有处理器寄存器的值,同时它的堆栈中存储着参数、返回地址以及变量之类的临时数据。在多处理机操作系统中,进程之间除了从属关系以外相对独立,如果系统中某个进程崩溃,不会影响到其余进程,每个进程运行在各自的虚拟地址空间中,通过一定的通信机制,它们之间才能发生联系。

进程描述符(进程控制块)

       为了对进程进行管理,Linux内核必须了解每个进程当前的执行状态,这些状态包括进程的优先级、运行状态、分配的地址空间等。为了达到这个目的,Linux内核提供了一个结构体task_struct来描述进程(或者说表示进程实体)。

struct task_struct {
    volatile long state;    //进程运行时的状态,-1表示不可运行,0表示可运行,大于0表示已停止
    unsigned int falgs;     //flags是进程当前的状态标识
                            //0x00000002表示进程正在被创建
                            //0x00000004表示进程正准备退出
                            //0x00000040表示进程被fork,但没有执行exec
                            //0x00000400表示此进程由于其他进程发送相关信号而被杀死
    unsigned int rt_priority    //进程的优先级
    truct list_head tasks;
    struct mm_struct *mm;    //内存的使用情况
    int exit_state;
    int exit_code, exit_signal;
    pid_t pid;    //Process ID
    pid_t tgid;    //进程组号
    struct task_struct *real_parent;    //该进程的创建者,即“亲生父亲”
    struct task_struct *parent;    //该进程现在的父进程,有可能是“继父”
    struct list_head children;    //指向该进程孩子的链表,可以得到所有子进程的进程描述符
    struct list_head sibling;    //指向该进程兄弟的链表,也就是其父进程的所有子进程
    struct task_struct *group_leader;    //进程组的组长
    struct list_head thread_group;    //该进程所有线程的链表
    time_t utime, stime;    //处理器相关的时间参数
    struct timespec start_time;    //进程启动时间
    struct timespec real_start_time;    //与上一条类似
    char comm[TASK_COMM_LEN];
    int link_count, total_link_count;    //文件系统信息计数
    struct thread_struct thread;
    struct fs_struct *fs;    //特定处理器下的状态
    struct files_struct *files;    //文件系统相关信息结构体

    //打开文件相关信息结构体
    struct signal_struct *signal;
    struct sighand_struct *sighand;

    //松弛时间值,用来规定select()和epoll()的超时时间,单位是纳秒
    unsigned long timer_slack_ns;
    unsigned long default_timer_slack_ns;
};

进程标识符

       进程标识符(Process ID)是进程描述符中最重要的组成部分,用于标识和对应唯一的进程。

       Linux内核使用了一个数据类型pid_t来存放进程标识符,这个数据类型实质上是一个机器相关的无符号整数(类似于size_t)。PID通常被顺序编号,且PID是可以重复使用的。当一个进程被回收之后,过一段时间,其标识符又可以被再次使用。为了和16位处理器架构的应用系统相兼容,在Linux内核上通常允许使用的进程标识符是0~32767。

       在Linux中,有如下几个特殊的进程标识符所对应的进程:

       ·PID0:对应的是交换进程(swapper),其用于执行多进程的调用。

       ·PID1:初始化进程(init),在自举过程结束时由内核调用,其对应的文件是/sbin/init,负责Linux的启动工作,这个进程在系统运行过程中是不会终止的(守护进程),可以说当前操作系统中的所有进程都是由这个进程衍生而来的。

       ·PID2:可能对应页守护进程(pagedaemon),用于虚拟存储系统的分页操作。

Linux进程的用户

       与文件类似的是,进程也有对应的实际用户ID、实际组ID、有效用户ID、有效组ID。对于这些用户而言每个进程同样存在一个相应的标识符,Linux提供了相应的函数用于获取这些标识符,对其标准调用格式说明如下:

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

uid_t getuid(void);    //获取实际用户ID
uid_t geteuid(void);   //获取有效用户ID
gid_t getgid(void);    //获取实际组ID
gid_t getegid(void);   //获取有效组ID

Linux进程操作

创建进程

       POSIX标准定义了进程创建函数fork和vfork以创建一个新进程,被创建的新进程称为当前执行该创建函数进程的子进程。

fork

       fork函数实质是一个系统调用,其作用是创建一个新的进程,当一个进程调用它完成后就出现两个几乎一模一样的进程,其中由fork创建的新进程被称为子进程,而原来的进程称为父进程。子进程是父进程的一个拷贝,即子进程从父进程得到了数据段和堆栈段的拷贝,这些需要分配新的内存;而对于只读的代码段,通常使用共享内存的方式访问。

       用户通常在有如下需求的时候使用fork函数:

       ·一个进程希望复制自身,从而使得父子进程能同时执行不同段的代码,通常来说这种应用会涉及网络服务:父进程等待远端的一个请求或应答,当收到这个请求或者应答的时候调用fork创建一个子进程来完成处理,而自己继续等待远端的请求或应答。

       ·进程想执行另外一个程序,例如在shell中调用用户所生成的应用程序。

#include <unistd.h>

pid_t fork(void);

       fork函数没有参数,它被调用一次,但是返回两次:

       ·对于父进程而言:函数的返回值是子进程的进程标识符(PID)。

       ·对于子进程而言:函数的返回值是0。一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得父进程的标识符,所以不需要在这里返回父进程的进程标识符。

       ·如果出错,返回值为“-1”。

数据空间共享

       当fork函数返回后,子进程和父进程都从调用fork函数的下一条语句开始执行,但是父进程或子进程哪个先执行是随机的,这个取决于具体的调度算法。

       通常来说,fork所创建的子进程将会从父进程中拷贝父进程的数据空间、堆空间和栈孔家,并且和父进程一起共享正文段,需要注意的是子进程所拷贝的仅仅是一个副本,和父进程的相应部分是完全独立的。

vfork

       在使用fork函数创建一个进程后,可以不适用exec系列函数来执行新的程序,如果要执行新的程序则必须调用exec系列函数。在这种情况下可以使用vfork函数,该函数在创建完一个新的进程后自动实现exec系列函数的功能。

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

pid_t vfork(void);

       fork与vfork之间的区别如下:

       ·fork要拷贝父进程的数据段,而vfork不需要完全拷贝父进程的数据段,在子进程没有调用exec系列函数或exit函数之前,子进程与父进程共享数据段。

       ·vfork函数会自动调用exec系列函数去执行另一个程序。

       ·fork不对父子进程的执行次序进行任何限制,而在vfork中,子进程先运行,父进程挂起,知道子进程调用了exec系列函数或exit之后,父子进程的执行次序才不再有任何限制。

执行进程

       在Linux中可以调用fork函数来创建一个子进程,该子进程几乎复制了父进程的全部内容,但是如果需要在子进程中执行一些自定义动作,则需要调用exec函数族。

       当调用exec系列函数的时候,该进程执行的程序被立即替换为新的程序,而新程序则从main函数开始执行,并用它来取代原调用进程的正文段、数据段、堆和栈,但其进程标识符和进程描述符是不会改变的。

       在Linux中通常会在如下两种情况下调用exec函数族:

       ·当进程不能再为系统和用户做出任何贡献时,就可以调用exec函数族让自己重生。

       ·如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用exec函数族中的一个函数,这样看起来就像通过执行应用程序而产生了一个正文段、数据段等都与其父进程不同的全新进程。

#include <unistd.h>

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 execvpe(const char *file, char *const argv[], char *const envp[]);

       上述exec函数族的参数说明如下:

       ·path:可执行目标文件的路径名

       ·file:可执行目标文件的文件名

       ·arg:目标文件的路径名

       ·argv:一个字符指针数组,由它指出该目标程序使用的命令行参数表,按照约定第一个字符指针指向与path或file相同的字符串,最后一个指针指向一个空字符串,其余的指向该程序执行时所带的命令行参数。

       ·envp:与argv一样也是一个字符指针数组,由它指出该目标程序执行时的进程环境,它也以一个空字符串结束。

       execl、execle、execlp这三个函数用于表示命令行参数的一般方式是:

char *arg0, char *arg1, ..., char *argn, (char *)0

退出进程

       当一个进程执行完成后必须要退出,退出时内核会进行一系列操作,包括释放缓冲区等。通常来说Linux的应用程序代码会调用exit系列函数来退出一个进程。

#include <stdlib.h>
#include <unistd.h>

void exit(int status);
void _exit(int status);
void _Exit(int status);

       exit系列函数没有返回值,其使用一个称为终止状态(exit status)的整形变量作为参数,Linux内核会对这个终止状态进行检查,当异常终止时,Linux内核会直接产生一个终止状态字,描述异常终止的原因,可以通过wait或者waitpid函数来获得终止状态字。父进程也可以通过检查终止状态来获得子进程的状态。如果main函数的返回值定义为整型并且main函数正常执行到最后一条语句返回,则终止状态是0。

       _exit与exit的区别:

       _exit:直接使进程停止运行,清除其占用的内存空间,并清除其在内核中的各种数据结构。

       exit:在_exit的基础上做了一些包装,在执行退出之前加了若干道程序。如调用前要检查文件的打开情况,把文件缓冲区中的内容写回文件(清理I/O缓冲)。

       当一个进程退出时,可能存在以下两种状态:

       ·其父进程恰好因忙于其他事务暂时不能接收子进程的终止状态,如果此时子进程完全消失,那么当父进程处理完其他事务想检查子进程的情况时就没有可用的信息了。所以Linux内核为每个已退出的进程保留一定的信息,一般至少包含进程标识符、终止状态字、进程处理器时间等信息。父进程可以通过调用wait或waitpid函数得到相应的信息,在此之后,Linux内核再将这些数据释放。通常把这种已经结束,但其父进程尚未检查其终止状态的进程称为僵尸进程。

       ·如果父进程可能先于子进程结束,此时init进程就会自动成为该子进程的父进程。

       由以上可知,当调用exit系列函数或者return函数返回时,其实进程并没有真正的完全消失,其还在继续占用部分资源。如果这种僵尸进程过多,就会大大影响系统的性能。

销毁进程

       当一个进程使用exit系列函数退出时,会在内存中保留部分数据以供父进程查询,同时也会产生一个终止状态字,然后Linux内核会发出一个SIGCHLD信号以通知父进程。因为父进程的结束对于父进程是异步的,因此这个SIGCHLD信号对于父进程也是异步的,父进程可以不响应。

       父进程对于退出后的子进程的默认状态是不处理的,这样会导致系统中的僵尸进程浪费了系统资源,此时应该调用wait或waitpid函数对这些僵尸进程进行处理。

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

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

       在调用wait或waitpid之后可能存在如下三种情况:

       ·如果该父进程的所有子进程都还在运行,则阻塞父进程自身以等待子进程的运行结束。

       ·如果有一个子进程已经结束,则父进程取得该子进程的终止状态,并且立即返回。

       ·如果该父进程没有任何子进程,则立即出错返回。

wait函数

       如果wait函数调用成功则返回子进程的标识符,如果失败则返回-1,其中参数status是一个整型指针,可以用于存放子进程的终止状态,也可以定义为一个空指针。

       wiat函数与waitpid函数不同,在有一个子进程终止之前,wait函数让父进程阻塞以等待子进程退出,而waitpid有一个参数可以让父进程不阻塞。并且在一个父进程有多个子进程的情况下,如果其中有一个子进程退出则会返回该子进程的进程标识符。

wait函数返回的宏
说明
WIFEXITED(status) 当子进程正常结束时返回为真
WIFSIGNALED((status) 当子进程异常结束时返回为真
WEXITSTATUS(status) 当WIFEXITED(status)为真时调用,返回状态字的低8位
WTERMSIG(status) 当WIFSIGNALED(status)为真时调用,返回引起状态终止的信号代号

waitpid函数

       在使用wait函数时,如果父进程的任何一个子进程返回则wait函数返回,而waitpid函数可以通过参数来指定需要等待的子进程。waitpid函数的参数pid用于对子进程进行相应的筛选:

       ·pid>0:只等待PID为pid的子进程,不管其他已经有多少子进程结束退出了,只要指定的子进程还没有结束,waitpid就一直等待下去。

       ·pid=-1:等待任何一个子进程退出,没有任何限制,此时waitpid等价于wait。

       ·pid<-1:等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

       waitpid函数的参数options用于进一步控制waitpid函数的操作,其可以是0,也可以是WNOHANG和WUNTRACED两个选项之一,或者是使用“|”符号连接的“或”操作。对这两个选项的定义如下:

       ·WNOHANG:如果由pid指定的子进程并不是立即可用的,则waitpid函数不阻塞,此时返回“0”。

       ·WUNTRACED:如果某实现支持作业控制,而由pid指定的任意子进程已经处于暂停状态,并且未报告过,则返回其状态。

       总体而言,waitpid函数提供了wait函数所没有的三个功能:

       ·能够等待一个指定的进程结束。

       ·能够不阻塞父进程获得子进程的状态。

       ·支持作业控制。

猜你喜欢

转载自blog.csdn.net/qq_37653144/article/details/81781837