Linux系统编程(四)—— 进程基本知识

一、进程标识符pid

1、pid的类型:pid_t

pid有符号的16位整型数,也就是说可以同时进行三万多进程

2、命令:ps

ps命令用于报告当前进程的信息:
在这里插入图片描述
ps命令有着不同的组合,可以显示进程不同的内容:

在这里插入图片描述

(1)ps -axf :是描述当前进程

在这里插入图片描述

(2)ps -axm :描述进程详细信息,m表示more

在这里插入图片描述

(3)ps ax -L :以linux特有的方式进行查看

在这里插入图片描述

3、进程号是顺次向下使用

注意: 之前讲的文件描述符,是优先使用当前最小的文件描述符,但是进程标识符不一样,进程号是顺次向下使用, 即使前面有释放的,进程号也会一直变大。

4、如何获得当前进程的进程号?

(1)getpid( )函数:获得当前进程的进程号

(2)getppid()函数:获得当前进程的父进程进程号

在这里插入图片描述

二、进程的产生

1、fork( )函数

  • (1)作用:创建一个新的进程,通过复制当前进程
    在这里插入图片描述
    注意理解关键字: duplicating, 意味着拷贝,克隆,一模一样

重点1:fork创建新的进程,是通过复制当前进程

复制标志着: 副本和原本是一样的, 是通过复制父进程的方式来创建进程的,连执行到的位置都一样。

  • (2)fork后父子进程的区别
    1)fork的返回值不一样
    2)pid不同
    3)ppid不同, 父进程ID
    4)未决信号(悬而未决)和文件锁不继承
    5)资源利用量归零

  • (3)init进程: 1号进程,是所有进程的祖先进程

  • (4)fork( )函数返回值
    fork( )函数运行成功后,会有两个返回值。当调用成功的时候,给父进程返回子进程的pid号, 返回给子进程0;如果创建失败,则返回父进程-1
    在这里插入图片描述

  • (5)例子:

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

int main()
{
    printf("[%d] begin!\n", getpid());

    pid_t pid = fork();
    
    if(pid < 0){ 
        perror("fork()");
        exit(1);
    }   
    
    if(pid == 0){    // child
        printf("[%d]: Child is working~\n", getpid());
    }else{           // parent
        printf("[%d]: Parent is working~\n", getpid());
    }   

    printf("[%d] End!\n", getpid());
    
    exit(0);

}

运行结果:
在这里插入图片描述
注意:永远不要去猜父进程和子进程哪一个被调度。调度器的调度策略来决定哪个进程先运行。这里也可能会出现子进程先调度。

重点2:永远不要去猜父进程和子进程哪一个被调度

如果想要子进程先运行,可以有一定的措施,但一定不要猜测(比如这里加了一个sleep函数-但是不推荐用这个函数,后面会解释)
在这里插入图片描述
运行结果:
在这里插入图片描述
在源代码中加入一句 getchar( )代码,让程序一直运行,不结束:
在这里插入图片描述
另外开一个终端,利用 ps -axf 命令,来查看当前进程的信息:
在这里插入图片描述
像这种阶梯状的都是父子进程的关系,这种顶格写的他们的父进程都是1号init

重点3:面试题->fflush的重要性

(begin是打印了一个,end一定是打印了两个,说明两个进程执行到的节点都是一样的)但是,如果将输出重定向到一个文件中,就会出现问题:
可以看到,这里出现了两个 Begin
在这里插入图片描述
为什么呢? 是因为在创建子进程的时候没有刷新缓冲区,导致,缓冲区中的数据没有更新。因此应该这样做:在fork()之前刷新所有该刷新的流
标准输出是行缓冲,文件是全缓冲,所以会出现打印终端上没有问题,但是输出到文件中时就有两个Begin) (全缓冲模式中 \n 只表示换行,没有刷新缓冲区的作用
在这里插入图片描述
运行结果:
在这里插入图片描述

2、例子:数质数的个数

(1)原始例子

题意:计算给定区间中质数的个数
代码:

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

#define LEFT   30000000
#define RIGHT  30000200

int main() {
    int i, j, mark;
    for (i = LEFT; i <= RIGHT; i++) {
        mark = 1;
        for (j = 2; j < i / 2; j++) {
            if (i % j == 0) {
                // 非质数
                mark = 0;
                break;
            }   
        }   
        if (mark)
            // 是质数
            printf("%d is a primer\n", i); 
    
    }   
    exit(0);
}

运行结果:
在这里插入图片描述

如果想要知道该程序执行了多少的时间,可以采用:time 命名

在这里插入图片描述

如果终端上只想显示运行了多少时间,但是代码却又输出了内容,可以将这些内容输出重定向到一个空设备上去:/dev/null (注意这里 time 不属于标准输出,不能重定向到空设备上去)
在这里插入图片描述

(2)改进例子

  • 如果把这个任务让多个子任务去操作, 创建200 个子进程去计算
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define LEFT   30000000
#define RIGHT  30000200

int main() {
    int i, j, mark;
    pid_t pid;
    
    for (i = LEFT; i <= RIGHT; i++) {
        pid = fork();
        if (pid < 0) {
            perror("fork");
            exit(1);
        }   

        if (pid == 0) {
            // i子进程,干活
            mark = 1;
            for (j = 2; j < i / 2; j++) {
                if (i % j == 0) {
                    // 非质数
                    mark = 0;
                    break;
                }   
            }
            if (mark)
                // 是质数
                printf("%d is a primer\n", i);
            exit(0); // 子进程结束(一定要有)
        }
    }
    exit(0);
}

运行结果:
(可以看到这是乱序的状况,这是因为进程调度的问题)

在这里插入图片描述

(3)对比

在这里插入图片描述

  • 可以看到前后的时间差距很大,你可能会想,程序0相当于一个人在干201个活, 程序1相当于一个人干一个活,共201个人干, 所以它的时间短,错了!
    因为当前系统是4核的,所以4个进程是可以平行运行的。如果是单核机器的话, 无论是多少个 进程,都要一个一个调度,所以单核机器使用的时间大小和有多少个进程没有关系

  • 注意:但是四核的机器运行的时间并不是单核机器运行时间的四分之一,所以运行时间还是和调度有很大关系。

  • 谁打开谁关闭, 谁申请,谁释放,父进程创建了子进程,那么父进程就是要给子进程“收尸

  • 如果程序当中出现僵尸态,僵尸态应该是一闪即逝的,因为表示及时收尸了; 或者如果你查看的时候有几个僵尸态进程,过了三五秒钟再看,还是有几个僵尸态,但是换了一批,这种情况表示要么就是当前操作系统比较忙,要么就是父进程比较忙,需要过一段时间进行批量的收尸。

3、vfork( )函数:将被废弃了

  • fork( )函数:是通过复制父进程的方式产生子进程,它和父进程一样,只有那五点不一样。

  • 例如,我的父进程中从数据库中导入30万条记录,创建子进程,然后让子进程打印hello world, 然后退出,这样的fork成本比较高,但是注意,这说的只是fork的原始实现

  • 如果使用vfork的话,当使用vfork产生一个子进程的话,子进程和父进程共用一个数据块,但是,如果子进程中修改了的,父进程可以看得到吗??man手册中说:使用vfork调用的进程,只能保证成功的调用_exit(2)或者exec(3) , 其他的都属于未定义范围。
    在这里插入图片描述

  • 其实现在fork已经不是前面说的那样了,fork添加了写时拷贝技术,在进行fork的时候,子进程与父进程确实是共用数据块(只读不写), 如果一个进程企图使用一个指针去写,首先会把数据拷贝一份,然后修改自己的那一份,不会影响其他的进程,谁改谁拷贝

三、进程的消亡及释放资源(收尸)

  • 收尸:等进程状态发生变化,然后对进程收尸,资源回收。

    前面执行primer1程序的时候,命令行会先出现,然后后面的打印后出现,这个现象的原因是???

1、wait( )函数(没有指向,只会死等)

在这里插入图片描述

(1)功能

父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
1) 阻塞等待子进程退出
2) 回收子进程残留资源
3) 获取子进程结束状态(退出原因)

(2)查看进程退出状态

pid_t wait(int *status) 将当前子进程收尸收回来的状态放入到一个整形变量status中,也就是说给他一个整形变量的地址值。函数提供了一系列的宏,用于查看当前进程退出的状态是什么
在这里插入图片描述

  • WIFEXITED(status) :判断当前进程是否正常终止,背五条正常终止,三条异常终止

  • WEXITSTATUE(status) :在进程必须正常结束情况下,返回子进程退出时的状态

  • WIFSIGNALED(status) :判断进程是否是由信号使其异常终止的

  • WTERMSIG(status):如果是由信号导致的进程异常终止,返回使进程终止的那个信号的编号

2、waitpid( )函数(不用死等)

在这里插入图片描述

(1)作用

作用同于wait,但可指定pid进程清理,可以不阻塞。
该函数好用的点在于可以使用 options 使得操作不阻塞(options是一个位图,其中一个NOHANG. 不用死等)

(2)参数介绍

  • 参数 pid:
    pid > 0 回收指定ID的子进程

    pid = -1 回收任意子进程(相当于wait)

    pid = 0 回收和当前调用waitpid一个组的所有子进程

    pid < -1 回收指定进程组内的任意子进程
    在这里插入图片描述
    注意:
    (1)用户分组,进程组什么的,不管什么分组,唯一的好处就是好操作。一个进程创建出来的进程是跟它的父进程是同组进程(当然可以人为设置使其不是同组)。
    (2)一次wait或waitpid调用只能清理一个子进程,清理多个子进程需要用到循环

  • 参数 status
    同wait函数一样,这里不做介绍了

  • 参数 options
    该参数是一个位图,可以由多个进行 与 操作,options的默认值是0,表示会阻塞。可以通过将 options 设置为常量 WNOHANGWUNTRACEDWCONTINUED
    的各种组合来修改默认行为:

    (1)WNOHANG: 如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。 默认的行为是挂起调用进程,直到有子进程终止 。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。(这个就可以使回收操作变成非阻塞)

    (2)WUNTRACED: 挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止 。返回的PID 为导致返回的已终止或被停止子进程的 PID。默认的行为是只返回已终止的子进程。当你想要检査已终止和被停止的子进程时,这个选项会有用。

    (3)WCONTINUED: 挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中一 个被停止的进程收到 SIGCONT 信号重新开始执行。

3、例子:利用wait( )函数进行收尸

代码:

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

#define LEFT   30000000
#define RIGHT  30000200

int main() {
    int i, j, mark;
    pid_t pid;

    for (i = LEFT; i <= RIGHT; i++) {
        pid = fork();
        if (pid < 0) {
            perror("fork");
            exit(1);
        } 
        if (pid == 0) {
            // i子进程,干活
            mark = 1;
            for (j = 2; j < i / 2; j++) {
                if (i % j == 0) {
                    // 非质数
                    mark = 0;
                    break;
                }
            }
            if (mark)
                // 是质数
                printf("%d is a primer\n", i);
            exit(0); // 子进程结束
        }
    }
    
    // int st;
    for (i = LEFT; i <= RIGHT; i++) {
        // wait(&st);
        wait(NULL); // 不保存状态)
    }
    exit(0);
}

运行结果:
在这里插入图片描述

四、进程分配

问题:如果当前的任务有201个,然后用于处理这个任务的进程有N(3)个,如何分配?

1、分块法

  • 三个进程,第一个进程处理一部分,第二第三个进程处理一部分。

  • 缺点是:任务的轻重,分配并不平均

2、交叉分配法

  • 第一个数给进程1, 第二个数给进程2, 第三个数给3, 第四个数给进程1, 第五个数给进程2…

  • 如果在一个任务中,如果可以使用分块法也可以使用交叉分配法的时候,我们使用交叉分配法。

例子:利用交叉分配法对任务进行分配

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

#define LEFT   30000000
#define RIGHT  30000200
#define N      3

int main() {
    int i, j, n, mark;
    pid_t pid;
    
    for (n = 0; n < N; n++) {
        pid = fork();
        if (pid < 0) {
            perror("fork()");
            // 这里注意,如果出错了,需要写一个循环,把曾经fork出去的内容全部收尸
            exit(1);
        }  
        
        if (pid == 0) {
            for (i = LEFT + n; i <= RIGHT; i += N) {
                mark = 1;
                for (j = 2; j < i / 2; j++) {
                    if (i % j == 0) {
                        // 非质数
                        mark = 0;
                        break;
                    }
                }
                if (mark)
                    printf("进程[%d] %d is a primer\n",n , i);
            }
            exit(0);
        }
    }

    for (n = 0; n < N; n++) {
        wait(NULL);
    }
    exit(0);
}

运行结果:
在这里插入图片描述

3、池类算法

  • 上游一些进程,下游一些进程,中间一个模块(池子),上游的进程将任务往中间模块扔,下游的进程抢任务。

  • 这样的任务分配和抢到的任务都具有随机性。

五、exec函数族

  • 在当前阶段,需要记住一个单词 "few", 这个单词的三个字母: f: forke: exec, w: wait, 这三个函数搭建起了linux的框架。

  • 疑问?? 为什么shell创建的子进程不是shell, 而是primerN这样的进程??

  • exec函数族的函数有(执行一个文件):execl( )、execp( )、execle( )、execv( )、execvp( ) 。

  • exec函数族用一个新的进程映像,替换当前的进程映像。
    在这里插入图片描述

  • 比如说: 进程空间搭建起来的话,在exec这个阶段就已经有代码段,已初始化数据段,未初始化数据段,栈和堆是后来才搭建起来,所以在c程序虚拟空间完成的时候,是在各个不同的阶段做的不同的实现,搭建起来的不同的数据内容。
    在这里插入图片描述

  • 注意: 上面的environ环境变量, 它的存储和argv的存储非常像。后面两个函数看起来是定参结构,前面两个是变参实现,但是实际上有多少个存储结构是和argv有关的,argv才是真正意义上的变参实现,所以前面两个函数是定参,后面两个是变参。

1、execl( ):定参

  • 格式: int execl(const char *pathname, const char *arg, ... /* (char *) NULL */);

  • 传入一个可执行文件的路径,char * arg …的意思是:要给这个命令传参的化,参数是哪些,可以传多个参数,最后补一个NULL作为当前传参的结束。

2、execlp( ):定参

  • 格式:int execlp(const char *file, const char *arg, ... /* (char *) NULL */);

  • 传入一个可执行文件的名字,char * arg …的意思是:要给这个命令传参的化,参数是哪些,可以传多个参数,最后补一个NULL作为当前传参的结束。

  • 为什么只传入一个可执行文件的名字不需要路径就可以呢??因为他有环境变量。环境变量是程序员与管理员之间的一种约定。

3、execle( ):定参

  • 格式:int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);

  • 这个函数和execlp差不多,最后可以将一个环境变量导入进来。

4、execv( ):变参

例子:查看时间戳

  • (1)执行命令行打印时间戳
    在这里插入图片描述

  • (2)利用 which 命令查看一下 date 命令在哪里
    在这里插入图片描述

  • (3)利用代码打印时间戳

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

/*
    date + %s
    打印时间戳
*/
int main() {
    puts("Begin");

    // 用一个新的进程映像来替换现在的
    // 如果下面的execl()函数执行成功,则不会打印End
    execl("/bin/date", "date", "+%s", NULL);
    // 这里都不用判断execl返回值,因为下面的内容只有在
    // execl执行失败之后才会执行到
    perror("execl()");
    exit(1);
    puts("End");
    exit(0);
}

执行结果:
在这里插入图片描述

  • (4)我们将这个函数的输出重定向到 /tmp/out , 然后在显示,会发现Begin没有了
    在这里插入图片描述
    所以需要注意:在exec这个函数族的使用的时候,一定也要注意fflush的使用。当缓冲区还没有向外输出呢,exec这个函数就用来替换当前的旧的进程映像。所以在使用exec之前一定要将所有的流刷新一下。
    在这里插入图片描述

  • (5)你还是你,但是你已经不是你了,它的壳子没有变(PID), 但是它的内容(进程映像)已经变了。Unix 世界是怎么做的, shell

    代码:


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

/*
    使用fork, wait, exec
*/

int main() {
    pid_t pid;
    puts("Begin");
    fflush(NULL);
    
    pid = fork();
    if (pid == 0) {
        // 子进程
        execl("/bin/date", "date", "+%s", NULL);
        perror("execl()");
        exit(1);
    }   
    // 父进程等着收尸
    wait(NULL);
    puts("End");
    exit(0);
}

运行结果:
在这里插入图片描述
所以所有的shell都是这样做的,当你执行一个命令的时候,shell会根据fork创建一个子进程,然后在子进程里面进行exec操作, 替换子进程,shell这个父进程在wait, 等待收尸。所以当我们执行一个命令的时候,都是命令的结果先出来,也就是子进程结果出来,然后命令行再弹出来,因为shell进程在wait,当子进程结束的时候,shell帮忙给收尸。

为什么是父子进程可以打印在同一终端上

  • 每个进程中有一个文件描述符表,0、1、2关联stderr, stdin, stdout, 一旦创建子进程,子进程是通过复制父进程来的,所以它的文件描述符表是和父进程一样的,所以终端(父进程)执行 一个命令(子进程)的时候,子进程是打印输出到终端的。

例子:执行sleep

代码:

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

/*
    使用fork, wait, exec
*/

int main() {
    pid_t pid;
    puts("Begin");
    fflush(NULL);

    pid = fork();
    if (pid == 0) {
        // 子进程
        execl("/bin/sleep", "sleep", "100", NULL);
        perror("execl()");
        exit(1);
    }   
    // 父进程等着收尸
    wait(NULL);
    puts("End");
    exit(0);
}

查看当前父子进程关系:
在这里插入图片描述

  • argv[0]很多时候被人忽略,但是如果换成其他的名字,比如,我们换为"httpd", 显示的就是httpd, 这就是木马的产生的低级办法。比较高级的隐藏在内核模块当中,当然没有隐藏在用户态危害更大。
    在这里插入图片描述父子进程关系在这里插入图片描述

六、用户权限及组权限

  • 其实用户权限和组权限是没有我们想的那么简单,它们是分作好几种方式来进行存放的,为什么这么做?

  • 当我们查看/etc/shadow的时候,用户权限是不够的;但是我们操作/etc/passwd权限是够的。
    在这里插入图片描述

  • 一开始的时候,普通用户什么权限也没有,连修改自己口令的权限都没有,权限全部集中在root用户,慢慢的,root用户的权限开始下放,普通用户才慢慢的有了权限。普通用户可以通过passwd文件修改自己的口令。

1、关于:u + s 和 g + s

  • 其实在我们执行某一个命令的时候,是带着一个身份来执行的,身份从哪来的?

  • uid 和 gid, 拿出一个来讲: uid
    user id(uid)其实存的不只有一份,它有三份。一个叫real uid, effecitive uid, save uid。可以没有save uid . 鉴定权限的时候是用effective id

  • exec鉴定权限, exec发现了u+s的权限, 看/etc/passwd 的权限。
    在这里插入图片描述
    前面讲到stat函数的时候,其中也有一位用来保存当前文件时否有u+s, g+s的权限,(s体现u+s权限, x体系g+s权限)

  • 注意:
    (1) u+s指的是: 如果一个文件有u+s的权限,那就意味着:当别的用户在调用当前这个二进制文件的时候,它的身份会切换成当前二进制文件的user的权限来执行。

    (2)g+s指的是:如果这个文件有g+s权限的话,那就意味着,当前不管任何用户来调用这个二进制可执行文件的时候,当前用户的身份就会切换成这个二进制文件的同组用户身份来执行。

    (3)exec来鉴定权限, 鉴定权限看的是effective ui。

    (4)所以当用:ls -l /usr/bin.passwd 的时候,其实是以root身份来跑。
    (5)其实u+s, g+s的作用 就是将原来root的权限打散往下放。

2、当前shell的身份从哪里来的

  • 当前机器中产生的第一个进程是init进程,当init产生的时候,本身还是root权限,所以init的权限是root权限
  • 然后init 进行fork+exec产生一个进程getty进程(root身份),getty进程提示你请输入login name, 然后我们输入用户名
  • 然后getty进程进行exec(注意没有fork子进程), 直接替换为login 进程,然后login进程提示你继续输入 passwd.
  • 如果口令验证成功,fork+exec 产生一个shell (这个shell是在passwd中根据用户名和密码找得到shell存在的路径)。
  • 如果失败,继续返回回去输入用户名密码。
  • shell身份固定以后再继续做任何时候,都是带着用户权限的身份去做的了
    在这里插入图片描述

3、权限相关的常用函数

(1)getuid( ) 返回进程实际进程ID

(2)geteuid( ) 返回进程的有效ID effective uid

(3)getgid( ) 获取当前进程的真实组ID

(4)getegid( ) 返回当前进程有效的组ID effective gid

(5)setuid( ) 设置有效ID

(6)setgid( ) 设置有效的组ID

(7)setreuid( ) 交换ruid 和 euid

(8)setregid( ) 交换rgid 和 egid

(9)seteuid( )

(10)setguid( )

七、解释器文件(脚本文件)

  • 解释器文件是有Unix代表性的味道,因为Unix在讲机制,策略,总是在告诉你我能怎么做,我在怎么做,我能完成什么样的功能,但是不会告诉你我能做到什么程度。

什么叫脚本文件

  • 脚本文件总是有一个脚本文件的标记,脚本文件其实不在乎后缀是什么,一般我们写shell脚本叫a.sh, Python脚本的话叫a.py, 当然这个后缀没什么关系

  • 脚本文件的标记就是文件顶头有一个 #! , 下面是你要用什么, 比如说 /bin/bash
    创建一个名为 aa.123 的脚本文件

#!/bin/bash

ls
whoami
cat /etc/shadow
ps
  • 注意: 中间一条命令的执行失败不shell影响其他的命令执行

我们看下aa.123 的属性:
在这里插入图片描述
然后给他改变权限, 给他可执行权限 chmod u+x aa.123
在这里插入图片描述
去执行这个脚本:
在这里插入图片描述
刚刚这个脚本文件等同于下面的操作:
在这里插入图片描述
其中的 test 文件为:
在这里插入图片描述

  • 脚本的优点就是: 我们有时候用c程序要写好大一段程序去解决的时候,shell几句话就解决了。不光是shell, Python脚本也一样,同时Python比shell 更灵活一些。

  • 当你的shell看到一个脚本文件的时候,它对带脚本文件和别的程序不一样,其他的程序的话,就会将整个程序装在进来,如果在装载程序的时候发现,最前面是脚本标记,也就是#! , shell就不把当前所有内容都装载进来了,只在当前shell环境下来装载解释器文件(也就是/bin/bash),然后用指定的解释器解释全部的内容,包括第一行(第一行的井号刚好就是注释)。

  • 这里的解释器不一定是shell, 我们把/bin/bash 改成/bin/cat , 结果如下:
    在这里插入图片描述
    运行结果:
    在这里插入图片描述
    当然,这也等同于:cat test
    在这里插入图片描述

八、system( )函数

在这里插入图片描述

  • 根据man手册查询可知,system( )函数就相当于一个 fork, exec, wait 的简单封装
  • system( )函数的小例子:
#include <stdio.h>
#include <stdlib.h>

int main() {
    system("date +%s > /tmp/out");
    exit(0);
}

运行结果:
在这里插入图片描述

  • 利用fork, exec, wait 的简单封装system函数:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    fflush(NULL);

    pid = fork();
    if (pid == 0) {
        // 子进程
        execl("/bin/sh", "sh", "-c", "date +%s > /tmp/out", NULL);
        perror("execl()");
        exit(1);
    }   
    // 父进程等着收尸
    wait(NULL);
    exit(0);
}

运行结果:
在这里插入图片描述
可以看到前后是一样的效果

九、进程时间:times( )函数

  • time ./primer1 是打印执行primer1的时间

  • 原理: 当前父进程在等子进程的时候在掐时间,所以说当前时间(等待子进程时间)包括等在子进程结束的时间,都会纳入到这个时间当中去。

  • times( ) 函数用来完成time命令的

在 man 手册中查询times( )函数:
在这里插入图片描述
计时单位:clock_t 滴答数, 一秒钟的滴答数可以用一个宏来检测,如下:
在这里插入图片描述
tms的四个时间碎片拼一起就是一个time命令。

十、守护进程

  • 守护进程是生存期较长的一种进程,一般在系统启动时启动,系统关闭时停止,没有控制终端,后台运行。

  • 守护进程可以理解成服务,一些系统级模块,开机启动的时候,需要进程一直在后台跑的,这种程序被称为守护进程,比如:Httpd守护进程, dhcp(动态分配IP地址), ssh服务,等等

  • 当我们执行ps命令的时候,看到的一些后台跑的内容,有很多本身就是守护进程。无论是windows还是linux, 都是有守护进程。

  • 守护进程一般满足如下条件:
    (1)脱离控制终端(因为控制终端的输入和输出会对它有影响)
    (2)会话的leader
    (3)group的leader

1、会话 session (终端)

  • 会话是一个或多个进程组的集合

  • 我们平时接触的都是虚拟终端,实际意义上的终端是一个笨设备。只会两个操作,输入和输出。一般在银行这种要求安全性比较高的地方,可能会有真正意义上的终端存在。

  • 在学并发这个模块的时候,就会发现,进程实际上就是容器,在我们的处理器处理当前的调度的时候,其实它是以线程为单位来调度的,所以认为多线程的并发要比多进程的并发要更规范
    在这里插入图片描述

  • 进程组分为:前台进程组+后台进程组,最多可以有一个前台进程组,可以没有前台进程组。

  • 如果一个程序运行的会久一些,我不希望它占着我的命令行,终端,我还会做别的事情,那我在这条命令后面加一个地址符号,也就意味着,当前这条命令我要放到后台去运行。

  • 前台进程组能够接受标准输入,能够标准输出,后台进程组不行。如果不区分前台进程组和后台进程组,我们不知道命令行输出的是哪个进程组的。

  • 所以我们的守护进程的标准的输入和输出我们会进行重定向。

2、setsid( )设置为守护进程

在这里插入图片描述

  • 如果当前调用这个函数的进程不是一个group leader的话,会创建一个新的session。调用这个函数的进程会成为新的session的leader。其实调用setsid()进程的特点就是守护进程的特点。

  • 利用 ps axj 命令可以查看工作的进程
    在这里插入图片描述

  • TTY表示的是控制终端,守护进程是脱离控制终端的, 所以打问号?的是守护进程

  • 利用setsid()之后,当前的进程会成为session的leader, 会成为group的leader, 所以说它的PID, PGID,SID是相同的

  • 由于守护进程在执行的时候,父进程会一直在等着,比如:FTP是从开机就开始,父进程一直在等着,既然子进程调用setsid()以后会变成守护进程,直接让父进程退出,父进程不需要收尸,如果父进程退出了,那么当前守护进程的父进程的PPID值就为1, init进程

十一、系统日志

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值