Linux应用开发之进程处理

简介        

        进程(Process)是正在运行的程序,是操作系统进行资源分配和调度的基本单位。程序是存储在硬盘或内存的一段二进制序列,是静态的,而进程是动态的。进程包括代码、数据以及分配给它的其他系统资源(如文件描述符、网络连接等)。例如在windos系统下打开一个浏览器或者其余任何一个应用程序,都属于一个进程,而程序本身并不是进程,只有当他在运行时才被称为进程。

system创建子进程

system是C标准库函数 int system(const char *command) ;

作用是把 command 指定的命令名称或程序名称传给要被命令处理器执行的主机环境,并在命令完成后返回。也就是根据传入的命令启动一个进程来执行这个命令。他可以自适应对应的操作系统,在windos和linux系统下都可以直接调用。

参数是传入的shell命令,以字符串形式传入,执行成功返回0,失败返回非0

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

#define handle_error(cmd,result)    \
        if(result < 0)              \
        {                           \
            perror(cmd);     \
            exit(EXIT_FAILURE);     \
        }                           \

int main(int argc, char const *argv[])
{
    int result = 0;

    result = system("ping -c 10 www.baidu.com");
    handle_error("system",result);
    return 0;
}

本段代码使用system创建一个子进程来执行ping -c 10 www.baidu.com这个命令。在程序运行后在终端中使用ps -ef 来查看所有进程。

如图第一个进程是执行makefile,第二个是运行system这个可执行文件(因为我把上述代码的.c文件命名为了system),他启动了子进程3723,子进程3723又启动了子进程3724来执行ping -c 10 www.baidu.com

进程处理

        进程处理主要涉及创建子进程,跳转到其他进程,等待进程结束等,包括fork,execve,waitpid函数。

        在进程处理中,程序的main函数主要采取有参数格式,int main(int argc, char *argv[]);

        argc是传递给程序的命令行参数的数量。

        argv指向字符串数组的指针,存储了命令行参数,是在命令行运行此程序时传入的参数。

        argv[0]通常是程序的名称。

        argv[1]到argv[argc-1]是实际的命令行参数。

        每个进程都有一个独立的编号,称为pid(process id),即进程ID,用于区分不同的进程,pid的数据格式为__pid_t或者pid_t,层层定义下来最终也就是int类型。

fork函数

        fork()函数的作用是创建一个子进程,并继承父进程的资源,即将父进程的资源复制了一遍,但是子进程也是一个进程,拥有独立的进程ID,创建这个子进程的进程叫做父进程,在调用fork之前,程序内只有父进程在运行,fork之后,程序就被复制了一遍,fork之后的程序在父子进程内都会执行一遍。

        fork函数在成功创建子进程之后会返回一个值,在父进程中会返回子进程的PID,在子进程中就会返回0,这就可以帮助我们区分父子进程,用于设置父子进程分别执行不同的功能。

        pid_t getpid(void);

        getpid函数会返回调用该函数的进程的进程ID。

        pid_t getppid(void);

        getppid函数会返回调用该函数的进程的父进程的进程ID

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

#define handle_error(cmd,result)    \
        if(result < 0)              \
        {                           \
            perror(cmd);     \
            exit(EXIT_FAILURE);     \
        }                           \

int main(int argc, char const *argv[])
{
    pid_t pid = 0;

    printf("现在是父进程\n");

    pid = fork();

    handle_error("fork",pid);
    
    if(pid == 0)//子进程
    {
        printf("现在是fork之后创建的子进程,进程ID为%d,我的父进程的进程ID为%d\n",getpid(),getppid());
    }
    else if(pid > 0)//父进程
    {
        printf("现在是fork之后的父进程,进程ID为%d\n",getpid());
    }

    printf("此段代码,父子进程都会执行\n");

    return 0;
}

程序运行结果如下

        可以看到在fork之前的程序只执行了一次,在fork之后如果不区分pid的值,程序就会在父子进程中都执行,且子进程可以正确返回父进程的ID。

文件描述符的引用计数

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

#define handle_error(cmd,result)    \
        if(result < 0)              \
        {                           \
            perror(cmd);     \
            exit(EXIT_FAILURE);     \
        }                           \

int main(int argc, char const *argv[])
{
    int fd = 0;
    fd = open("io.txt",O_RDWR | O_CREAT,0666);
    handle_error("open",fd);

    pid_t pid = 0;

    char write_buf[60] = "父进程在fork之前写入的\n";

    write(fd,write_buf,strlen(write_buf));

    pid = fork();
    handle_error("fork",pid);

    if(pid == 0)//子进程
    {
        strcpy(write_buf,"子进程在fork之后写入的\n");
        write(fd,write_buf,strlen(write_buf));
    }
    else if(pid > 0)
    {
        strcpy(write_buf,"父进程在fork之后写入的\n");
        write(fd,write_buf,strlen(write_buf));
    }

    close(fd);

    return 0;
}

程序运行结果如下,查看io.txt文件

        在这个程序中,经过fork创建子进程之后,子进程并没有再单独打开io.txt文件,而是可以直接使用父进程打开的文件描述符fd从而向文件中写入数据。这是因为子进程复制了父进程的文件描述符fd,二者指向的应是同一个底层文件描述(struct file结构体)。

        如果子进程通过close()释放文件描述符之后,父进程对于相同的文件描述符执行write()操作仍然可以成功。这是因为struct file结构体中有一个属性为引用计数,记录的是与当前struct file绑定的文件描述符数量。close()系统调用的作用是将当前进程中的文件描述符和对应的struct file结构体解绑,使得引用计数减一。如果close()执行之后,引用计数变为0,则会释放struct file相关的所有资源。通过fork创建子进程,子进程复制父进程的文件描述符,在这一过程中,会使对应文件的引用计数+1,所以在关闭时,父子进程都需要调用close关闭文件描述符,才能使其引用计数降为0。否则struct file相关的所有资源将不会释放。

execve

        exec是一个系列函数,可以在一个进程中跳转到另外一个函数并开始执行,如果跳转成功,那么该函数之后的内容都不会再执行,而是跳转到新程序开始执行新程序的内容,跳转之后的进程ID保持不变。本文以execve为例。

        函数原型为int execve (const char *__path, char *const __argv[], char *const __envp[]);

char *__path: 需要执行程序的完整路径名    

char *const __argv[]: 指向字符串数组的指针 需要传入多个参数        

(1) 需要执行的程序命令(和*__path相同)        

(2) 执行程序需要传入的参数        

(3) 最后一个参数必须是NULL    

char *const __envp[]: 指向字符串数组的指针 需要传入多个环境变量参数,环境变量也可以不传,直接赋值为NULL        

(1) 环境变量参数 固定格式 key=value        

(2) 最后一个参数必须是NULL    

return: 成功就回不来了 下面的代码都没有意义             失败返回-1

        先写一个跳转到的目标程序,编译为可执行文件,命名为execve_dst

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

int main(int argc, char const *argv[])
{
    if(argc < 2)
    {
        printf("参数小于两个\n");
    }
    else if(argc >= 2)
    {
        printf("参数正常,跳转到的进程ID为%d\n",getpid());
    }
    return 0;
}

        再写一个跳转的程序,跳转到execve_dst,该程序命名为execve_src

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

int main(int argc, char const *argv[])
{
    
    char *argvs[] = {"./execve_dst","跳转的源头",NULL};
    char *envp[] = {NULL};

    printf("我是跳转源头,我的进程ID是%d\n",getpid());

    execve("./execve_dst",argvs,envp);

    printf("我已经跳转了\n");

    return 0;
}

        编译运行execve_src,结果如下

        可以看到两次调用getpid查询到的进程ID相同,且execve之后的printf没有执行。

waitpid

        Linux中父进程除了可以启动子进程,还要负责回收子进程的状态。如果子进程结束后父进程没有正常回收,那么子进程就会变成一个僵尸进程——即程序执行完成,但是进程没有完全结束,其内核中的相关资源没有释放。如果父进程在子进程结束前就结束了,那么其子进程的回收工作就交给了父进程的父进程的父进程。本次使用waitpid使父进程等待子进程结束后回收资源后再结束,防止子进程变为僵尸进程。即时子进程使用了exit()结束进程,也需要使用waitpid回收子进程。

        子进程调用exit会主动结束进程并返回相关状态,释放文件描述符、内存等用户空间的资源,但是内核资源(如进程描述符PCB)仍未回收,此时子进程变为了僵尸进程。所以仍需waitpid对子进程进行回收,waitpid可以释放子进程残留的内核相关资源(PCB等),彻底终止子进程。

        wait函数 

        原型为pid_t wait(int *wstatus);

        作用是等待子进程停止并获取退出状态。

        waitpid函数

        原型为pid_t waitpid(pid_t pid, int *wstatus, int options);

pid: 等待的模式

 *      (1) 小于-1 例如 -1 * pgid,则等待进程组ID等于pgid的所有进程终止

 *      (2) 等于-1 会等待任何子进程终止,并返回最先终止的那个子进程的进程ID -> 儿孙都算

 *      (3) 等于0 等待同一进程组中任何子进程终止(但不包括组领导进程) -> 只算儿子

 *      (4) 大于0 仅等待指定进程ID的子进程终止

wstatus: 整数指针,子进程返回的状态码会保存到该int

options: 选项的值是以下常量之一或多个的按位或(OR)运算的结果;二进制对应选项,可多选:

 *      (1) WNOHANG 如果没有子进程终止,也立即返回;用于查看子进程状态而非等待

 *      (2) WUNTRACED 收到子进程处于收到信号停止的状态,也返回。

 *      (3) WCONTINUED(自Linux 2.6.10起)如果通过发送SIGCONT信号恢复了一个已停止的子进程,则也返回。

          若填0则为waitpid最初的功能,等待子进程关闭。

return: (1) 成功等到子进程停止 返回pid

 *         (2) 没等到并且没有设置WNOHANG 一直等

 *         (3) 没等到设置WNOHANG 返回0

 *         (4) 出错返回-1

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

#define handle_error(cmd,result)    \
        if(result < 0)              \
        {                           \
            perror(cmd);     \
            exit(EXIT_FAILURE);     \
        }                           \

int main(int argc, char const *argv[])
{
    pid_t pid = 0;

    pid = fork();
    handle_error("fork",pid);

    if(pid == 0)
    {
        sleep(10);
        printf("子进程马上结束\n");
    }
    else
    {
        printf("父进程正在等待子进程结束\n");
        waitpid(pid,NULL,0);
        printf("父进程等待子进程结束,即将结束\n");
    }
    return 0;
}

        程序的运行结果应该是先打印出父进程正在等待子进程结束,等待十秒打印出子进程马上结束,之后子进程结束,父进程等待子进程结束后打印父进程等待子进程结束,即将结束。

进程树

        Linux的进程是通过父子关系组织起来的,所有进程之间的父子关系共同构成了进程树(Process Tree)。进程树中每个节点都是其上级节点的子进程,同时又是子结点的父进程。一个进程的父进程只能有一个,而一个进程的子进程可以不止一个。

        ps -ef可以查看当前系统中的所有进程,我们在程序中调用命令行输入来阻塞程序内容,然后在终端中使用ps -ef命令查看进程的ID号和父进程的ID号,最终会查到ID为1的进程,实质上,1号进程就是systemd,它由内核创建,是第一个进程,负责初始化系统,启动其他所有用户空间的服务和进程。它是所有进程的祖先。

        查看进程树的命令是pstree,会以树状图展示所有用户线程的依赖关系,就像一张族谱图

        调用pstree -p可以显示每个进程的ID号

孤儿进程

        孤儿进程(Orphan Process)是指父进程已结束或终止,而它仍在运行的进程。

        当父进程结束之前没有等待子进程结束,且父进程先于子进程结束时,那么子进程就会变成孤儿进程。

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

#define handle_error(cmd,result)    \
        if(result < 0)              \
        {                           \
            perror(cmd);     \
            exit(EXIT_FAILURE);     \
        }                           \

int main(int argc, char const *argv[])
{
    pid_t pid = 0;

    pid = fork();
    handle_error("fork",pid);

    if(pid == 0)
    {
        printf("子进程刚刚创建,进程ID为%d,我现在的父进程ID为%d\n",getpid(),getppid());
        sleep(5);
        printf("子进程已经休眠了5s,我现在的父进程ID为%d\n",getppid());
    }
    else if(pid > 0)
    {
        printf("父进程的ID号为%d\n",getpid());
        sleep(2);
        printf("父进程已经结束\n");
    }

    return 0;
}

        根据结果可以看到,父进程结束之后,程序已经是退出状态,运行结束,子进程仍未结束,又重新占用了终端输出,而且其父进程变为了祖先的ID,所以孤儿进程会被其祖先自动领养。此时的子进程因为和终端切断了联系,所以很难再进行标准输入使其停止了,所以写代码的时候一定要注意避免出现孤儿进程。

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值