Unix中的进程和线程-1

目录

1.如何创建一个进程

2.如何终止进程

2.2遗言函数

3.进程资源的回收

4.孤儿进程和僵尸进程

[孤儿进程 (Orphan Process):](#孤儿进程 (Orphan Process):)

[僵尸进程 (Zombie Process):](#僵尸进程 (Zombie Process):)

代码示例:

[5. 进程映像的更新](#5. 进程映像的更新)


在Linux中,进程和线程是操作系统进行工作调度的基本单位,它们各自拥有特定的属性和作业环境。 进程是指在系统中正在运行的一个应用程序。每个进程间都有各自独立的内存空间,即使是同一个程序的不同实例也不会共享这些信息。比如说,你可以打开多个计算机浏览器,它们都是独立的进程。 每个进程至少有一个线程,这个线程被称为主线程。线程是进程中的一个单独的序列流,是进程的实际操作单位。线程在同一进程的上下文中运行,并共享同一进程的命名空间,这意味着它们共享同样的代码段、数据段和开放的文件资源等。 关于进程和线程之间的关系,你可以将进程看作是土地,而线程则像是在土地上进行工作的人。每块土地(进程)上可以有一个或多个工人(线程),他们共享土地资源(进程的内存空间和资源),但各自进行不同的工作。 总结一下,进程提供运行环境的上下文,而线程则在这个上下文中实际执行任务。线程的切换比进程的切换开销更小,因此使用多线程可以在同一进程内同时执行多项任务,提高程序的效率。

1.如何创建一个进程

在unix中我们使用fork函数来创建当前进程下的子进程:

fork() 是一个非常重要的系统调用。它被用来创建新的进程。这个新的进程被称为子进程,而创建子进程的进程被称为父进程。 fork() 函数的独特之处在于,它只被调用一次,但却返回两次。它在父进程中返回新创建子进程的进程 ID,而在子进程中则返回 0。如果在调用 fork() 期间发生错误,例如无法获取到必要的内核资源,那么就会返回一个负数。 当 fork() 创建子进程时,子进程是父进程的一个复制品。它会获得父进程的数据空间、堆和栈的复制品。这意味着父进程中的数据和状态将会被复制到子进程中。然而,父进程和子进程的执行环境是独立的,他们有各自独立的进程ID,要传递信息需要使用特定的进程间通信(IPC)机制。 但是,值得注意的是,虽然子进程复制了父进程的上下文,但一些值,如一些系统资源(例如打开的文件描述符)、进程标识符(PID)、父进程标识符(PPID)等会被改变。 命名为 fork() 的函数名代表着"分叉"的意思,恰当的描述了创建新进程的行为,即从一个进程分出一个新的进程,这两个进程之后将会独立执行。

(查看手册)

使用例子:

cpp 复制代码
#include<t_stdio.h>
#include <unistd.h>
int main(){
        pid_t pid;
        //在父进程中创建一个子进程
        pid = fork();
        if(pid==-1)E_MSG("fork",-1);
        if(pid==0){//子进程执行的代码
        printf("child process..\n");
        }
        else {//{父进程执行的代码
        printf("parent process ..\n");
        }
        //父进程和子进程都能执行的代码

        printf("all ok..\n");
        return 0;
}

2.如何终止进程

我们常用return 和exit来终止一个进程:

使用例子:当前进程会停在gechar()这,这时我们查看我们当前进程与父进程的关系:

cpp 复制代码
#include <stdlib.h>
#include<stdio.h>
int main(void){
        getchar();
        exit(1);

}

这里说明我们执行程序a.out的时候,其父进程是bash,我们使用exit(status)就会返回status给父进程:

2.2遗言函数

cpp 复制代码
#include<t_stdio.h>
#include <stdlib.h>
 #include <unistd.h>
void bye(void){
        printf("bye bye..\n");
        return;

}
void goodbye(void){
        printf("good bye..\n");
        return;
}
int main(void ){
        //向进程注册遗言函数
        atexit(bye);
        atexit(bye);
        atexit(goodbye);
        //创建子进程
        pid_t pid=fork();
        if(pid==-1)E_MSG("fork ",-1);

        getchar();
        return 0;
}

先是父进程的遗言函数,如何是子进程的遗言函数。

3.进程资源的回收

cpp 复制代码
#include<t_stdio.h>
#include <sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
#include <sys/types.h>
int main(void ){
        //创建子进程
        int pid;
        pid = fork();
        if(pid==-1)E_MSG("fork .. ",-1);
        if(pid==0){//子进程执行代码
        printf("child process.. %d\n",getpid());
        exit(0);
        }
        else {//父进程执行代码
        //父进程阻塞等待回收子进程的资源
        wait(NULL);
        printf("parent process..\n");
        }


        return 0;
}

只有子进程被终止父进程才会终止:

同样还有另一个回收函数:

具体使用可以参见手册。

4.孤儿进程和僵尸进程

在Linux中,孤儿进程和僵尸进程是两种不同的状态,它们都与进程的生命周期和父子进程关系相关。下面我会分别解释这两种状态,并且提供一个代码例子进行详细分析。

孤儿进程 (Orphan Process):

当一个父进程终止或者退出时,它的子进程可能还在运行。这时,子进程将被 init 进程(进程号为1)收养。收养后的子进程会成为 init 进程的孩子,成为孤儿进程。孤儿进程不再有父进程,因此由 init 进程负责对其进行处理。

僵尸进程 (Zombie Process):

当一个进程完成了执行,但其父进程还没有通过 wait() 或 waitpid() 系统调用来获取其终止状态时,该进程会进入僵尸状态。僵尸进程的进程控制块仍然存在,但是进程已经没有任何执行代码,也没有分配到 CPU 时间片。僵尸进程不占用内存空间,但会占用一定的进程号(PID)资源。

代码示例:

下面是一个简单的 C 语言示例代码,演示了孤儿进程和僵尸进程的情况:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    pid_t child_pid = fork();

    if (child_pid > 0) { // Parent process
        printf("Parent process is running with PID: %d\n", getpid());
        sleep(5); // Sleep for a while to allow child process to become orphan
        printf("Parent process is terminating\n");
    } else if (child_pid == 0) { // Child process
        printf("Child process is running with PID: %d, Parent PID: %d\n", getpid(), getppid());
        sleep(10); // Sleep for a while to become zombie
        printf("Child process is terminating\n");
    } else { // Error handling
        perror("fork");
        exit(EXIT_FAILURE);
    }

    return 0;
}

在这个例子中,父进程创建一个子进程,然后父进程会休眠5秒后退出。在此之前,子进程还在运行,但父进程已经退出,因此子进程会成为孤儿进程。而子进程会休眠10秒后退出,这时它会成为僵尸进程,因为父进程没有处理子进程的退出状态。

要观察进程状态,你可以使用 ps 命令,例如 ps aux | grep <process_name>,其中 <process_name> 是你的程序的名称,或者使用 top 命令观察进程列表。

5. 进程映像的更新

在Unix系统中,"进程映像更新"是一个与程序执行密切相关的概念。它通常指的是当一个进程调用 exec系列函数(例如: execv, execvp, execp, execl, execlp, execle等)时,它原本的程序映像(即内存中的代码和数据)会被新的程序映像所取代,这个过程就被称为"进程映像更新"。 一个典型的例子是通过 shell 运行命令。当你在 shell 中输入一个命令并且按下回车的时候,shell会创建一个子进程,并在这个子进程中调用 exec 来执行你输入的命令。而在 exec 执行完成后,这个子进程的映像就被替换为你输入命令的程序映像。

进程映像更新与父子进程关系密切,因为往往在子进程中执行 exec() 系列函数来代替原有程序。对于 bash shell 命令或者其他需要新建进程来执行的程序,都是新建了子进程,然后在子进程中执行执行 exec() 系列函数来替换新的程序。 下面这个 C 语言例子就是一个典型的父子进程与 exec() 函数相结合的例子,展示了这种动态的父子进程交替和进程映像更新:

cpp 复制代码
#include<t_stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void){
        //创建子进程
        pid_t pid=fork();
        if(pid==-1)E_MSG("fork",-1);
        if(pid==0){
         printf("child process..\n");
        //更新进程的映像
        execlp("ps","ps","-o","pid,ppid,pgrp,comm","NULL");
        //只有exec执行失败了才会执行以下代码
        perror("execlp");
        exit(0);
        }
        else{
        wait(NULL);
        }

        return 0;
}

这段代码首先创建了一个子进程,然后在子进程中替换原有进程映像以运行 ps 命令。父进程则等待子进程终止。 下面是我对各处代码的详细解释:

  • #include <t_stdio.h>:这是一种非标准的头文件,可能是一个特定环境的定制头文件。通常我们使用标准库 <stdio.h> 来进行输入输出操作。

  • #include <unistd.h>:这个头文件提供了对POSIX操作系统 API 的访问。包括fork(), exec()等函数。

  • #include <sys/types.h>#include <sys/wait.h>:这两个头文件用于支持进程控制相关的数据类型和函数。

  • pid_t pid = fork();:使用 fork() 函数创建了一个新的子进程。

  • if(pid == -1)E_MSG(\"fork\",-1);:如果 fork() 函数返回 -1,表示创建失败,此时调用 E_MSG() 函数处理错误。

  • if(pid == 0){}:如果 fork() 函数返回值为 0,表示当前处于子进程环境。

  • execlp(\"ps\",\"ps\",\"-o\",\"pid,ppid,pgrp,comm\",\"NULL\");:调用 execlp() 函数在子进程中运行 ps 命令。参数 "ps" 是要执行的命令,接下来的参数是传递给 ps 命令的参数。

    • "ps":表示要运行的是 ps 命令,这个命令用于查看进程状态。
    • "-o":这个参数用于指定 ps 命令输出的格式。
    • "pid,ppid,pgrp,comm":这是 -o 参数后的具体格式,表示输出的是进程ID,父进程ID,进程组ID,以及命令名。
    • "NULL":这应该表示参数列表的结尾,但 "NULL" 这样的字符串并不会被 execlp() 函数识别为参数列表结束,通常我们使用 NULL 或者 (char*) 0 来表示参数列表的结束。
  • perror(\"execlp\");:如果 execlp() 函数调用失败,则会执行到这条语句,显示错误信息。

  • exit(0);:子进程结束运行,返回 0 表示正常退出。

  • wait(NULL);:这是在父进程中的语句,父进程调用 wait() 函数等待子进程结束。NULL 表示父进程不关心子进程的退出状态。 大致上,这个程序的作用就是创建一个子进程,然后在子进程中运行 ps -o pid,ppid,pgrp,comm 命令,输出相关的进程信息,之后子进程退出,父进程在子进程退出后也结束运行。
    exec() 函数实际上是一系列相关函数的函数家族(通称 exec 函数族或者 exec 系列函数),包括 exec, execp, execv, execvp, execve, execvpe 这些函数。这些函数主要用于在进程中加载和运行新的程序,它会替换当前进程的映像为新的程序映像,新程序从 main 函数开始执行,直到 main 函数返回或者调用 exit 结束。 下面我将逐一解释这些函数及其参数:

  • exec(): exec 是其他函数的基础,但通常我们不直接使用它,而是使用它的一些封装版本。这个函数会替换当前进程映像为新的程序映像。

  • execv() 和 execvp(): 这两个函数接受两个参数。第一个参数是我们要运行的程序的路径。第二个参数是一个字符指针数组,该数组包含新程序的命令行参数,最后一个指针后面必须是一个 NULL 指针。这两个函数的区别在于, execvp() 会使用环境变量 PATH,去寻找我们要执行的程序。

  • execve() 和 execvpe(): 这两个函数比上面的函数多一个参数,即新程序的环境变量的列表。这个参数同样是一个字符指针数组,数组中的每个字符串都有 NAME=VALUE 的形式,最后一个指针后面必须是一个 NULL 指针。 execvpe() 同样会使用环境变量 PATH 去寻找程序。 总的来说, exec 系列函数用于在当前进程中启动新的程序,完全替代了原有的程序并重新开始执行。所以它通常用于实现程序间的切换。这些函数只有在出错时返回,如果正常执行,调用 exec 系列函数的代码之后的部分都不会被执行,因为已经被新程序替换了。如果出现错误,比如文件不存在或者权限不足等,那么就会返回 -1,并设置 errno 为相应的值来表示错误原因。

相关推荐
龙哥说跨境4 分钟前
如何利用指纹浏览器爬虫绕过Cloudflare的防护?
服务器·网络·python·网络爬虫
七星静香12 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员13 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU13 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie617 分钟前
在IDEA中使用Git
java·git
Elaine20239132 分钟前
06 网络编程基础
java·网络
G丶AEOM33 分钟前
分布式——BASE理论
java·分布式·八股
落落鱼201334 分钟前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀35 分钟前
LRU缓存算法
java·算法·缓存
镰刀出海38 分钟前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试