linux 进程详祥祥解

前言

        程序,是一个存储在磁盘中的可执行文件,它是静态文件,而运行在内存中的程序就叫做进程,它是动态文件,一个程序包含一个或多个进程,每个进程都有唯一的进程号,一个进程中又包含一个或多个线程。

1. linux系统下进程

1.1 查看进程

1.1.1  ps命令

格式:ps [options]

常用参数:

-a:显示所有终端下执行的程序,包括其他用户的进程

-A:显示所有程序

-e:此选项的效果和指定"A"选项相同

 e:显示环境变量

-u:显示当前用户的进程状态

 f :显示程序之间的关系

-l :详细的格式来显示程序状况

-x:显示所有程序,不以终端机来区分

-H:显示树状结构

常用参数组合:

ps -aux:查看全部进程。

USER:进程所属用户、PID:进程号、%CPU:进程cpu占用率、%MEM:进程内存占用率、

VSZ:虚拟内存使用大小(kbyte)、RSS:占用固定内存大小(kbyte)、

STAT:进程当前状态(D:不可中断、R:运行态、S:休眠、T:暂停、Z:僵死态)、

START:进程运行起始时间、TIME:进程运行态时间、COMMAND:程序指令

 ps -elf

 pstree:以树形结构显示;

1.1.2 top命令

使用top命令可以实时监控进程状态,默认每3秒钟更新一次。类似Windows下的任务管理器。

1.2  关闭进程

通过查找要关闭进程的进程号PID,再使用 kill -9  PID 杀死指定进程。

2. linux进程

2.1 进程概述

程序是指令、数据及其组织形式的描述,进程是程序的实体,进程是系统进行资源分配和调度的基本单位。每个进程都有自己的独立虚拟地址空间,进程与进程、进程与内核互不干扰,一个进程不能读取或修改另一个进程或内核的内存数据,因为它们映射的物理空间不同。进程号为1(init)的进程是所有其它进程的父进程。

进程可以分为:

前台进程:该进程由终端运行并控制,终端与进程间可以进行实时交互,例如信息输入等;

后台进程:该进程没有与终端交互,例如执行运行指令后面添加“&”,该进程就会在后台运行;ctrl + z也会让进程暂停为后台进程。

2.2 进程组成

进程包含进程控制块(PCB)、代码区数据区

其中进程控制块结构体为struct  task_struct(include/linux/sched.h),主要用于表示进程相关信息,包含程序计数器、CPU寄存器、存储器管理、IO状态等。

32位的linux系统中,每个进程都有4GB的虚拟地址空间,其中0~3GB为独有的用户空间(独有空间),3~4GB是内核空间(内核空间为各进程间共享)。

每个进程都有与其相关的环境变量,环境变量可以从父进程继承过来,例如,shell终端启动一个程序,那么这个进程就会继承该shell终端的环境变量。程序中也可以调用库函数来获取环境变量。

2.3  虚拟地址转换

进程的虚拟地址通过MMU转换为物理地址。

先说下使用虚拟地址带来的好处,不需要考虑内存分配、内存不足的问题,就算实际内存只有2GB,进程也有4GB的虚拟地址空间可以使用。

linux中在程序还没有运行之前,程序指令和数据的虚拟地址是分配好的,而非分配好物理地址,程序运行时,linux系统提供给MMU进程的页目录和页表,将虚拟地址映射为物理地址,程序运行的时候如果还没有将需要的指令或者数据映射到物理内存中,就会不断的产生缺页异常,通过缺页异常处理函数将分配内存资源,进而完成内存映射。因此2GB的物理内存通过不断的缺页异常,不断重新分配内存资源。

下图中,两个不同的进程用户空间的虚拟地址相同,但映射的物理地址不同,它们内存空间是共享的。

2.4 进程创建

2.4.1  fork()创建进程

头文件:

#include <unistd.h>

函数原型:

pid_t fork(void)

功能:

创建一个进程

返回值:

成功:在父进程中返回子进程号,子进程中返回 0

失败:返回 -1

头文件:

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

函数原型:

pid_t getpid(void)               //获取进程号

pid_t getppid(void)             //获取父进程号

我们把原进程叫做父进程,fork创建的进程叫做子进程,它们是父子关系,拥有独立的进程空间,父进程和子进程共享同一个代码段,但不共享其它存储空间,子进程会拷贝父进程的数据段、堆、栈以及继承父进程打开的文件描述符等,它们互不影响,可以修改各自的数据段。

调用fork创建子进程后,会产生一个竞争问题,就是我们无法知道父进程和子进程谁会先占用CPU,这个时候可以使用信号来处理。

举例说明:

编写测试例程,让其在后台运行;程序中调用fork函数创建一个进程,调用成功它会有两个返回值,如下所示,子进程中返回值为0,并打印子进程号、父进程号和数据值,父进程中返回值大于0(创建的子进程号值),打印父进程号、pd返回值和数据值。从打印信息可以看出父进程或者子进程修改各自的value值。

使用ps命令同样可以查询到进程号,验证了其正确性。

演示代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>    //fork函数
#include <stdlib.h>    //exit函数

int main()
{
    int value = 19;
    pid_t pd;
    pd = fork();
    if(pd < 0){
        printf("fork error!\n");
        exit(0);
    }
    else{
        if(pd == 0){
            printf("this is son process:father pid = %d,son pid = %d,value = %d\n",getppid(),getpid(),value);
            value = 97;
        }
        if(pd > 0) {
            printf("this is father pid = %d,pd = %d,value = %d\n",getpid(),pd,value);
            value = 28;
        }
    }

    while(1);
    return 0;
}

fork通常有两个应用场景

① 调用fork函数创建子进程,使子进程与父进程有相同的代码段,两个进程执行不同的逻辑代码;

② 调用fork函数创建子进程,子进程中通过调用exec函数族执行与父进程不同的代码段。

2.4.2 vfork与fork区别

头文件:

#include <sys/types.h>

#include <unistd.h>

函数原型:

pid_t vfork(void)

vfork函数和fork函数的功能都是一样的,创建一个进程。但vfork函数做了优化,创建的子进程不会复制父进程的地址空间,子进程直接调用exec函数族组建新的进程空间;虽然vfork有些场景效率更优秀,但是它有时会产生莫名的bug,所以通常还是使用fork函数创建进程。

2.5 终止进程

进程的终止分为两种:正常终止和异常终止。

正常终止:调用return、exit()、_exit()、_Exit()函数来终止进程;

异常终止:调用abort()函数,或者接收到终止信号等。

调用exit()退出进程前,它会执行若干操作,最后系统调用exit退出进程。如果打开了文件,直接调用_exit()退出,则缓冲区的数据就会丢失,调用exit()才能保证数据的完整性。

2.5.1  wait函数、waitpid函数

wait、waitpid函数的主要功能是回收已经终止的子进程占用的内存空间,获取它们是如何终止的信息等。

头文件:

#include <sys/types.h>

#include <sys/wait.h>

函数原型:

pid_t wait(int *status)

参数:

status :用于存放子进程退出时的状态信息

返回值:

成功:返回成功退出的子进程号

失败:返回 -1

头文件:

#include <sys/types.h>

#include <sys/wait.h>

函数原型:

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

参数:

pid: pid >0,表示只等待进程号为pid的子进程退出,而不管其它的子进程;

         pid =0,表示等待该进程下的所有子进程退出;

         pid =-1,表示等待该进程下的任一个子进程退出;

         pid <-1,表示等待进程组标识符与pid绝对值相等的所有子进程;

status:用于存放子进程退出时的状态信息;

options:是一个位掩码,可以包括0个或多个如下标志;

        WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,返回值为0表示没有状态改变。

        WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息。

wait()是父进程用来等待子进程的终止,并获取子进程的终止状态。如果调用wait函数后没有子进程终止,则wait一直阻塞;如果调用wait函数前就有子进程终止了,则调用wait后立刻返回,并获取其终止状态信息。将参数status设为NULL表示不获取退出状态

wait 测试例程

在原进程(父进程)中使用fork函数创建一个子进程,子进程返回值中打印子进程号和父进程号,然后子进程就会执行while循环体,在父进程返回值中调用wait函数等待子进程的退出,子进程退出后打印退出的子进程号,然后父进程在循环体中退出。

后台运行该程序,然后在shell终端使用 kill -9  进程号  指令终止子进程,看现象如下图:父进程wait阻塞直到子进程终止。

//例程展示

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t pd =-1,ret = -1;
    pd = fork();
    if(pd < 0){
        printf("fork error!\n");
        exit(0);
    }
    else{
        if(pd == 0){
            printf("this is son process:father pid = %d,son pid = %d\n",getppid(),getpid());
        }
        if(pd > 0){
            ret = wait(NULL);
            printf("PID:%d process exit.\n",ret);
        }
    }
    while(1)
    {
        if(ret >0)
            break;
    }
    return 0;
}

 wait函数只能阻塞监视那些被终止的子进程,对于子进程其它状态就显得无能为力了,例如不能监视被暂停的子进程,而且一直阻塞意味着父进程不能执行其它指令了。

waitpid函数功能上就强于wait函数,配合参数的使用,可以达到很多效果,waitpid(-1,NULL,WNOHANG)表示可以非阻塞的监控任意子进程的状态变化。

测试例程:

对wait的例程稍加修改,后台运行程序,父进程中循环调用waitpid(-1,NULL,WNOHANG),如果子进程状态无任何变化,则该函数返回0,否则返回变化状态的子进程号。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t pd = -1,ret = -1;
    pd = fork();

    if(pd < 0){
        printf("fork error!\n");
        exit(0);
    }
    else{
        if(pd == 0){
            printf("this is son process:father pid = %d,son pid = %d\n",getppid(),getpid());
        }
        if(pd > 0){
            ret = 1;
        }
    }
    while(1)
    {
        if(ret >= 0){
            ret = waitpid(-1,NULL,WNOHANG);
            if(ret != 0){
                printf("PID:%d process exit.\n",ret);
                exit(0);
            }
            else
                sleep(1);
        }
    }
    return 0;
}

2.6 僵尸进程、孤儿进程

通过在进程中调用fork函数创建一个子进程后,子进程与创建它的进程就互为父子进程,它们有不同的生命周期。

2.6.1 僵尸进程

当子进程先于父进程终止时,终止的子进程在系统内存空间中还占用了一些空间,通常需要父进程调用wait或waitpid去处理它,回收它的空间,如果不去处理回收它,那么这个已经“死掉”的子进程就叫做僵尸进程。

测试例程

子进程返回值中让子进程终止,而父进程不去回收子进程,可以发现子进程即使已经退出了,终端中ps命令查看进程状态,子进程处于"Z"状态,即僵尸态。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t pd = -1;
    pd = fork();

    if(pd < 0){
        printf("fork error!\n");
        exit(0);
    }
    else{
        if(pd == 0){
            printf("this is son process:father pid = %d,son pid = %d\n",getppid(),getpid());
        _exit(0);
        }
    }
    while(1)
    {
        ;
    }
    return 0;
}

2.6.2  孤儿进程

当父进程先于子进程终止时,子进程就变成了孤儿,我们把它就做孤儿进程。此时孤儿进程就会被PID为1的 init 进程收养。

在Ubuntu图形界面下测试例程,你会发现父进程终止后,子进程的收养进程并不是init进程,而是CMD为(/sbin/upstart --user)的进程,如果在字符界面下测试就是init进程。

linux系统下proc文件系统下的那些数字目录代表各进程,找到对应进程的目录下的status就可以查看该进程的状态信息了。

2.7 守护进程

2.7.1  概念

守护进程即daemon进程,是linux系统的后台服务进程,它在系统载入时启动,直到系统关闭或者接收到某个信号才会关闭,即使控制终端关闭了,守护进程也不会关闭,从这可以看出它与系统共存亡,生命周期很长,独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生

2.7.2 相关术语

进程组ID:标识进程属于哪个进程组的ID,一个或多个进程组建成一个进程组,进程组的生命周期是从创建开始直到该组的所有进程都结束。

每个进程仅存在于一个进程组中,每个进程组都有一个组长进程,组长进程的进程号就是该进程组的ID号,且组长进程不能再创建进程组,新进程默认会继承父进程的进程组ID。

一个进程只能为它自己或它的子进程(没有调用exec函数族的子进程)设置进程组ID。

头文件:

#include <unistd.h>

函数原型:

pid_t getpgid(pid_t pid)        //pid:指定要进程号

pid_t getpgrp(void)

功能:

获取进程的进程组ID。

返回值:

成功:返回进程组ID;

失败:返回 -1。

头文件:

#include <unistd.h>

函数原型:

int setpgid(pid_t pid, pid_t pgid)        //将pid指定的进程的进程组ID设置为gpid

int setpgrp(void)        //等价于setpgid(0, 0)

参数:

setpgid()中如果pid与pgid相等,则以pid进程为组长创建新的进程组;

pid为0,则设置调用进程的进程组ID;

pgid为0,则以调用进程的pid创建新的进程组。

功能:

加入一个现有的进程组或创建一个新的进程组。

会话:一个会话包含一个或多个进程组,会话生命周期始于用户登录,终于用户退出。

一个会话只能有一个前台进程组,其余都是后台进程组;控制终端上的输入和信号都会发送给前台进程组。前台进程组ID做为会话的标识(即会话ID)。

 头文件:

#include <unistd.h>

函数原型:

pid_t getsid(pid_t pid)

参数:

        pid = 0:返回调用进程的会话ID;

        pid不为0,返回指定进程的会话ID;

功能:

获取会话ID;

返回值:

成功:返回会话ID;

失败:返回 -1。

头文件:

#include <unistd.h>

函数原型:

pid_t setsid(void)

功能:

创建一个新会话;

返回值:

成功:返回新会话ID;

失败:返回 -1

2.7.3 守护进程编写步骤

  创建子进程,并退出父进程。父进程退出,该子进程就会变为孤儿进程,从而被进程号为1的init进程收养,init进程变为该子进程的父进程。

子进程中使用setsid函数创建新会话。fork调用的时候子进程继承了父进程的进程组,现在调用setsid函数使子进程脱离原属的会话,创建一个全新的会话,子进程也成为了新进程组的组长进程,这样子进程就可以脱离原会话的控制了,从此原会话的生死都影响不了子进程了,它已经脱离了那个主体,自立为王了。

当前工作目录更改到根目录。我们都知道,假如你要删除的目录下程序在运行,你是删不掉这个目录的。通常将“/”根目录作守护进程的工作进程。可以调用chdir()函数更改工作目录。

重设文件权限掩码。一个文件的实际权限是由mode & (~umask)控制的,mode是调用open函数设置的权限位。shell终端中可以使用umask命令查看权限掩码。所以通常我们把权限掩码设置为0,确保守护进程有最大的操作权限,增加灵活性。

关闭文件描述符。子进程继承了父进程打开的文件描述符,父进程退出后,如果有文件描述符没有被关闭,子进程就要去关闭它,避免占用系统资源,导致无法卸载被打开的文件,包括文件描述符0,1,2也要关闭。

测试例程:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    pid_t pd = -1;

    pd = fork();    //第一步

    if(pd < 0){
        printf("fork error!\n");
        exit(0);
    }
    else{
        if(pd > 0){
            printf("father process exit.\n");
            exit(0);
        }
    }

    if(setsid() < 0)    //第二步
        exit(0);

    if(chdir("/") < 0)    //第三步
        exit(0);

    umask(0);    //第四步

    for(int i = 0; i < getdtablesize(); i++){    //第五步
        close(i);
    }

    while(1)
    {
        printf("hello.\n");    //终端无打印信息
        sleep(3);
    }
    return 0;
}

2.8 exec函数族

exec函数族并不是指一个函数,而是指代一类函数,这类函数功能相同,参数有些差异,函数均为“exec”开头,所以叫它exec函数族。

使用fork函数创建子进程后,通常会在子进程中调用exec函数族来执行其它应用程序。

exec函数族的主要作用就是,使其它可执行文件的代码段、数据段、堆栈等替换掉该进程(或者说子进程)的内存空间,但PID不会改变。这个可执行文件可以是脚本文件,也可以二进制文件。

execve()为系统调用,其它为库函数,这些库函数最终都是基于execve()实现的。

头文件:

#include <unistd.h>

函数原型:

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

参数:

        filename:表示新载入的可执行文件路径名;

        argv[ ]    :指定传递给新程序的命令行参数,argv[0]为新程序路径名,以NULL结尾;

        envp[ ]   :新程序的环境变量;

返回值:

        成功:无返回值;

        失败:返回 -1。

可以将exec函数族简单分为两类execlexecv“l”表示把传递的参数都列举出来,以"NULL"结束,“v”表示通过字符串数组的方式传递,“e”表示环境变量,“p”表示PATH路径。

函数区别:

第一个参数:path:传入路径名(绝对路径或相对路径);

                      file:传入可执行文件名;如果传入值包含“/”,则将其视为路径名;

第二个参数:arg:列举出各参数;

                      argv:传递字符串数组,同样以"NULL"结束;

第三个参数:envp:指定环境变量。

int execl(const char *path, const char *arg, ...);        //省略号表示后面跟多个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[ ]); 

测试例程

exec_app的测试源码如下:

/*********   exec_app 例程  *************/

#include <stdio.h>
#include <unistd.h>

int main(int argc,char *argv[])
{
    for(int i = 0; i< argc; i++){
        printf("argv[%d] = %s\n",i,argv[i]);
    }
    return 0;
}

execl:execl("./exec_app","./exec_app","hello","qurry","97",NULL);

第一个参数为新可执行文件路径,其它参数为传递给新可执行文件的参数,以NULL结尾。

新可执行文件退出后,父进程没有回收,使该子进程变为僵尸进程。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t pd = -1,ret = -1;
    pd = fork();

    if(pd < 0){
        printf("fork error!\n");
        exit(0);
    }
    else{
        if(pd == 0){
            printf("this is son process:father pid = %d,son pid = %d\n",getppid(),getpid());
            execl("./exec_app","./exec_app","hello","qurry","97",NULL);
        }
    }
    while(1);
    return 0;
}

execv:execl("./exec_app",argv);第二个参数为字符串数组。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t pd = -1,ret = -1;
    char *argv[5] = {"./exec_app","hello","qurry","97",NULL};
    pd = fork();

    if(pd < 0){
        printf("fork error!\n");
        exit(0);
    }
    else{
        if(pd == 0){
            printf("this is son process:father pid = %d,son pid = %d\n",getppid(),getpid());
            execl("./exec_app",argv);
        }
    }
    while(1);
    return 0;
}

execlp:execlp("ls","ls","-l",,NULL);

它会继承父进程的环境变量,到系统路径中查找可执行文件。

也可以使用命令 whereis  可执行文件 ,查找对应可执行文件的路径。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t pd = -1,ret = -1;
    pd = fork();

    if(pd < 0){
        printf("fork error!\n");
        exit(0);
    }
    else{
        if(pd == 0){
            printf("this is son process:father pid = %d,son pid = %d\n",getppid(),getpid());
            execlp("ls","ls","-l",,NULL);
            //execl("/bin/ls","ls","-l",NULL);
        }
    }
    while(1);
    return 0;
}

execvpe:execvpe("cat",argv,environ);

environ是一个系统维护的全局变量,是一个存放环境变量的字符串数组,应用程序中使用它前要声明。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

extern char **environ;

int main()
{
    pid_t pd = -1,ret = -1;
    char *argv[3] = {"cat","./exec_app.c",NULL};
    pd = fork();

    if(pd < 0){
        printf("fork error!\n");
        exit(0);
    }
    else{
        if(pd == 0){
            printf("this is son process:father pid = %d,son pid = %d\n",getppid(),getpid());
            execvpe("cat",argv,environ);
        }
    }
    while(1);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_49576307/article/details/128545156