Linux进程初识

169 阅读12分钟

一.进程的基本介绍

1.进程概念

在我门常常写代码的时候,大家有没有想过代码是如何运行起来的呢?

实际上我们编译的代码可执⾏⽂件只是储存在硬盘的静态⽂件,运⾏时被加载到内存,CPU执⾏内 存中指令,这个运⾏的程序被称为进程。

可以说,进程是运行时程序的封装,从用户角度上看,进程是程序的一个执行实例,可以看作是正在执行的程序,而从内核角度上看,进程是OS进行资源分配的最小单位。

2.如何描述进程

我们直到,在Linux中,对一切的事物都是先描述再组织,对进程也是如此。

操作系统管理进程也是一样的,操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的,当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理。 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block)。

同时,为了维护这些PCB,OS中维护了一张表格--进程表。该表项包含了⼀个进程状态的重要信息:

  • 标识符:用来描述本进程的唯一标识符。
  • 状态: 任务状态,退出代码,退出信号等
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器:程序中即将被执行的下一条指令的地址。
  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据: 进程执行时处理器的寄存器中的数据。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
  • 其他信息:如堆栈指针、所打开⽂件的状态。

有了这些信息,当CPU从该进程切换到其他进程,再切换回来的时候,就可以继续无障碍执行任务,就像从未被中断过一样。

3.查看进程

3.1proc文件夹

说了这么多,让我们切实查看一下进程。

image-20231209162249546

在系统的根目录下,有一个proc文件夹,里面存放着当前运行的所有进程信息,其中有些用数字表示,这个其实是某一进程的pid,用ls指令,就可以查看进程内存放的详细信息了。

image-20231209162520190

3.2ps指令查看

ps aux
//ps aux会查看所有的进程信息,并列出它们的拥有者、时间、状态等待

image-20231209162754643

也可以搭配grpe指令使用,查看某一进程信息。

3.3系统调用获取进程的pid和ppid

# makefile
test : test.c
	gcc -o $@ $^
.PHONY:clean
clean:
	rm -f test
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
//getpid函数
int main()
{
    while (1)
    {
        printf("I am process, pid is : %d, ppid is : %d\n", getppid(), getppid());
        sleep(1);
    }
    return 0;
}

image-20231209164001359

通过调用pid()和ppid()函数,就能知道该进程的pid和父进程的pid,这也表明,我们是可以手动创建一个进程,再创建它的子进程。

二.创建进程--fock()

2.1初始fork函数

fork是一个系统调用级别的函数,其功能就是创建一个子进程,示例代码如下:

pid_t fork(void)
//fork有两个返回值
//在父进程中返回新创建的子进程的pid,
//在子进程中返回0,
//执行失败返回-1,
//注意,父子进程没有固定的执行顺序,完全看OS的操作调度
//生成的子进程pid往往紧密连接父进程的,
//认识fork
int main()
{
    fork();
    while (1)
    {
        printf("I am %d, my father is %d\n", getpid(), getppid());
        sleep(1);
    }
    return 0;
}

image-20231209164901300

既然父子进程在fork函数的返回值不一样,那么我们就可以利用这点让父子进程去做不同的事,如下代码所示:

int main()
{
    printf("I am running\n");
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        while (1)
        {
            printf("I am child, my pid is: %d\n", getpid());
            //printf("my father is: %d\n", getppid());
            sleep(1);
        }
    }
    else if (id > 0)
    {
        // 父进程
        while (1)
        {
            printf("I am father, my pid is : %d\n", getpid());
            sleep(1);
        }
    }
    else
    {
        printf("error !\n");
    }

    return 0;
}

image-20231209193641103

2.2深入理解fork函数

1.父进程和子进程的代码数据如何分享?

父子进程虽然代码共享,但是父子进程的数据各自开辟空间(采用写时拷贝)。即当子进程还不需要空间时,我就不给你拷贝,等到你需要时,再拷贝,可以节省空间,提高效率。

2.为什么fork要给子进程返回0,父进程返回子进程的PID?

一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。

3.为什么fork会有两个返回值?

正常逻辑下,函数执行完,应该会返回一个返回值,但是父进程在执行fork函数时,在返回pid之前,子进程就创建好了,由于父子进程的代码共享,所以父进程不仅要执行,子进程也要执行return。

2.3写时拷贝

1.为什么数据要写时拷贝?

进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。

2、为什么不在创建子进程的时候就进行数据的拷贝?

子进程不一定会使用父进程的所有数据,且在不对子进程数据写入的情况下,也没必要对数据进行拷贝,最好时按需分配,延时分配,等需要修改数据时再分配,这样可以高效率地使用内存空间。

3.代码会不会进行写时拷贝?

90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。

三.进程状态

3.1进程优先级

我们之前说过,一个进程创建处理,保存了很多信息,其中就有进程的优先级。

1.所以,什么是进程的优先级?

优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。

2.优先级存在的原因?

优先级存在的主要原因就是资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。

3.查看进程的优先级

ps -l
//在Linux中,可以通过ps -l指令查看当前目录下的进程的详细信息

image-20231209204257106

  • UID:代表执行者的身份。
  • PID:代表这个进程的ID。
  • PPID:父进程ID
  • PRI:代表这个进程可被执行的优先级,其值越小越早被执行。
  • NI:代表这个进程的nice值。

3.2PRI和NI

  • PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高。
  • NI代表的是nice值,其表示进程可被执行的优先级的修正数值。
  • PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI。
  • 调整进程优先级,在Linux下,就是调整进程的nice值。
  • NI的取值范围是-20至19,一共40个级别。
  • 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。
  • Linux中可以通过top指令,和renice指令改变进程的nice值来调整优先级,不过优先级的调整是由限制的。

3.3有关进程的一些概念

  • 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级

  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。

  • 并发:单个核⼼在很短时间内分别执⾏多个进程,称为并发

  • 并行:多个核⼼同时执⾏多个进程称为并行

  • 对于并发来说,CPU需要从⼀个进程ڔ换到另⼀个进程,这个过程需要保存进程的状态 信息

3.4进程状态分类

一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器,有时虽有空闲处理器但因等待某个时间的发生而无法执行,这一切都说明进程和程序不相同,进程是活动的且有状态变化的,于是就有了进程状态这一概念。

static const char *task_state_array[] = {
	"R (running)",       /*  0*/
    "S (sleeping)",      /*  1*/
    "D (disk sleep)",    /*  2*/
    "T (stopped)",       /*  4*/
    "T (tracing stop)",  /*  8*/
    "Z (zombie)",        /* 16*/
    "X (dead)"           /* 32*/
};
//按照Linux源码,可以将进程状态大致分为以上几类,当然也有一些特殊状态

1)运行态-R

一个进程处于运行状态(running),并不意味着进程一定处于运行当中。运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程。

所有处于运行状态的进程,都被放在运行队列里,当OS需要切换进程的时候,就直接在运行队列里选取进程。

2)浅度睡眠态-S

一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠。

int main()
{
    printf("I am running\n");
    sleep(100);
    return 0;
}

image-20231213220843786

image-20231213220856156

我们可以看到,在调用sleep函数后,进程的状态为S+,即浅度睡眠。

3)深度睡眠态-D

一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束

4)暂停态-T

在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行,示例如下

ps aux | head -1 && ps aux | grep test | grep -v grep
//查看指定进程,proc为运行的文件名
int main()
{
    printf("I am running\n");
    sleep(1000);
    return 0;
}
//同样的代码我们让他运行起来,然后暂停他

image-20231213221905341

image-20231213222534884

5)僵尸态-Z

a.概念

当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态(zombie)。

设计僵尸进程的目的就是为了维护子进程的信息,以便父进程在某个时候获取,这些信息至少包括进程ID、进程的终止状态,以及进程使用CPU的事件,所有当终止子进程的父进程调用wait或者waitpid函数就可以获取这些信息。

为什么每次我们调用main函数都要返回0?其实这个0是返回给OS的,表示顺利退出,它自然也可以返回别的值。

echo $?
//Linux下获得最近一次进程退出时的返回码
b.僵尸进程

僵尸进程简而言之,就是处于僵尸状态的进程,接下俩我们创建一个僵尸进程感受一下:

//僵尸进程
//子进程打印5次以后退出,父进程一直循环
int main()
{
    printf("I am running ...\n");
    pid_t t = fork();
    if(t == 0)
    {
        //子进程
        int count = 5;
        while(count--)
        {
            printf("I am child, pid is: %d, ppid is: %d, count is: %d\n", getpid(), getppid(), count);
            sleep(1);
        }
        printf("I am child, I exit !!!!!!\n");
        exit(1);
    }
    else if(t > 0)
    {
        while(1)
        {
            printf("I am father, pid is: %d, ppid is: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else{
        //错误
    }

    return 0;
}

image-20231213231248465

image-20231213231305785

可以看到,由于子进程的数据一直没有被读取,一直处于僵尸态。

c.僵尸进程的危害
  • 僵尸进程的状态要被保存在PCB中,PCB需要一直维护,占用CPU资源
  • 子进程无法回收,就会一直占用内存,同时由于该内存一直被占用,但是没有实际作用,造成内存泄漏。

6)死亡态-X

死亡状态其实 是一个返回状态,当一个进程的退出信息被读,该进程所申请的资源就会立即被释放,该进程也就不存在了,所有我们一般不会在列表中看到死亡态(dead)。

7)跟踪态-TS

进程还有一种特殊状态,跟踪态(tracing stop),“tracing ”表示该进程正在被跟踪,“stop”表示该进程处于暂停态,跟踪态和暂停态有一样的态度,即处于暂停,但不同的是跟踪态无法响应信号,只能听从跟踪它的进程的指令,常常用于调试。

8)孤儿进程

在Linxu中的进程关系大多数都是父子关系,但是有的时候也会出现特殊情况-僵尸进程和孤儿进程。当子进程退出但是父进程没有读取,子进程就会变成僵尸进程。那反过来呢?即父进程先退出,等为未来子进程退出后就没有进程认领子进程,此时进程成为孤儿进程。

同样的,一直不处理孤儿进程,进程会一直占用资源,此时就会造成内存泄漏,但不同的是,出现孤儿进程以后,它会被1号进程init领养,然后转变为僵尸进程被init处理回收,示例如下:

//孤儿进程
int main()
{
    printf("I am running...\n");
    pid_t t = fork();
    if(t  == 0)
    {
        //子进程
        int count = 10;
        while(count--)
        {
            printf("I am child, pid is: %d, ppid is: %d, count is: %d\n", getpid(), getppid(), count);
            sleep(1);
        }
    }
    else if(t > 0)
    {
        //父进程
        int count = 5;
        while(count--)
        {
            printf("I am father, pid is: %d, ppid is: %d\n", getpid(), getppid());
             sleep(1);
        }
        printf("I am father, I exit!!!!!!\n");
        exit(0);
    }
    else{
        //错误
    }
    return 0;
}

image-20231213233947624

9)守护进程

守护进程是一类特殊进程,在后台运行的,没有控制终端和他相连,它独立于控制终端,周期性地执行某些任务,比如我们Linux的服务器,web服务器的进程http等等。

3.5进程状态转换图

  • 创建态:创建了新进程
  • 就绪态:进程获得了可以运行的所有资源和准备条件
  • 运行态:进程正在CPU中执行
  • 阻塞态:进程因为等待某项资源而被暂时移出CPU
  • 终止态:进程消亡

四.简单认识进程调度

由上面几个概念我们可以知道,进程之间是相互竞争的,而CPU处理某一个进程的时间是有限的,当该进程的处理时间结束以后,CPU就会切换到下一个进程,那么CPU是通过什么样的方法来判断优先级调度进程呢?

4.1批处理系统中的调度

1)先来先服务

⾮抢占式的调度算法,按照请求的顺序进⾏调度。

有利于⻓作业,但不利于短作业,因为短作业必须⼀直等待前面的⻓作业执⾏完毕才能执⾏, ⽽⻓作业⼜需要执⾏很⻓时间,造成了短作业等待时间过⻓。

2)最短作业优先

非抢占式的调度算法,按估计的运行时间最短的顺序调度,

缺点就是长作业可能饿死,因为一直有短作业到来那么长作业永远无法执行。

3)最短剩余时间优先

最短作业的抢占版本,按剩余运行时间最短的顺序进行调度。

当一个新作业到达时,其整个运行时间与当前进程的剩余时间做比较,如果新的进程需要的时间更少,则挂起当前进程,运作新的进程,否则新的进程等待。

4.2交互式系统中的调度

1)时间片轮转调度

将所有进程按照一定的原则排成一个队列,每次调度时,把CPU时间分配给队首进程,该进程执行完一个时间片以后,由计时器发出中断,调度程序将该进程送至就绪队列的末尾,再继续整个过程。

2)优先级调度

为每一个进程分配一个优先级,按优先级进行调度,为了防止低优先级的进程永远等不到调度,可以随着时间增加优先级。

3)多级队列

一个进程如果需要执行100个时间片,如果采用时间片轮转调度算法,则需要交换100次,多级队列就是为了这种需要连续交换多个时间片的进程考虑。

设计了多个队列,每个队列的时间片大小不相同,如1,5,10...。进程在当前队列没有执行完,则会移动到下一个队列,由此减少交换的次数,提高效率。

同时每个队列的优先级也不同,最上层的队列优先级最高,也只有当上一个队列没有进程在排队,才能调度下个队列。

4)最短进程优先

顾名思义,把每一个进程看作一个独立的作业,然后首先选择运行最短的作业来使得响应时间最短。

五.进程退出

经过前面对进程的了解,我们应该可以感觉到,进程并不是单单创建完就行了,它的退出也包含了很多信息,接下来讲解下有关进程退出的一些基本知识。

5.1进程退出场景

进程退出的情况,无外乎以下三种场景:

  • 1)代码运行完毕,结果正确。
  • 2)代码运行完毕,结果不正确
  • 3)代码异常终止,进程崩溃

5.2引入进程退出码

之前我们提到过,main函数最终的返回值其实也是一个退出码,它会告诉OSmain函数的退出情况。

实际上,main函数知识用户级别代码的入口,它是被其他函数调用的,例如在VS2013中,main函数被——tmainCRTStartup的函数调用,而该函数又是通过加载器被OS调用的,换言之,main函数间接被OS调用。

既然如此,那main函数退出时也会返回给OS对应的退出信息,这就是main函数返回值的作用,一般返回0表示代码成功执行完毕,非0表示错误,而我们有的时候代码出bug,退出码就是非0,根据错误的不同退出码也不同。

#include <stdio.h>
#include<string.h>
//展示C语言的进程退出码
int main()
{
    int i = 0;
    for(; i < 150; i++)
    {
        printf("error is %d: %s\n", i, strerror(i));
    }
    return 0;
}

image-20231214152659580

进程的退出码可以帮助我们判断代码执行失败的原因,但是错误码是人为规定的,不用环境下错误码的含义可能不一样。

进程实际上就是代码的一个执行流,所以对于main函数,它的返回值也就是该进程的进程退出码。

5.3exit函数和_exit函数

1.exit函数

之前的很多代码示例中我们都用过exit函数,实际上它是进程退出的常用方法,它可以在代码的任何地方退出进程,在exit函数退出进程前它会进行以下工作:

  • 1).执行用户通过atexit或者on_exit定义的清理函数。
  • 2).关闭所有打开的流,所有的缓存数据均被写入。
  • 3).调用_exit函数终止进程
//exit退出进程
void show()
{
    //打印一行数据,不换行,数据留在缓冲区
    printf("I am here !!!!");
    exit(0);
}

int main()
{
    show();
    return 0;
}

image-20231214153555166

如上,exit退出时,刷新了缓冲区。

2._exit函数

//_exit函数和exit函数类似,但不同的是,_exit函数会直接终止进程,而不进行任何收尾工作。
//_exit退出进程
void show()
{
    //打印一行数据,不换行,数据留在缓冲区
    printf("I am here !!!!");
    _exit(2);
}

int main()
{
    show();
    return 0;
}

image-20231214154055825

5.4return、exit和_exit函数直接区别和联系

1)区别

只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。

2)联系

return num等同于exit(num),因为在main函数结束以后,会将main函数的返回值当作exit的参数,进而调用exit函数退出进程。

六.进程等待

讲解僵尸进程的时候,我们说过,子进程在退出时会变成僵尸进程等待父进程读取信息,不读取,就不能退出,一直占用资源,因此回收进程的退出信息是非常有必要的,通常使用进程等待的方式回收子进程。

6.1status参数

在进程的信息中,有一个参数status,表示进程的退出信息,尽管status是一个整形变量,但是蕴含的信息很多。

在status的低16位比特位中,高8位表示退出状态,即退出码。如果进程被信号所杀,则低7位表示终止信号,第8位位core dump标志。core dump机制称为“核心存储”,记录了程序异常时的内存数据,寄存器状态以及堆栈信息等。

需要注意,当一个进程被信号所杀,那么它的退出码也就没有意义了。

6.2获取status

//通过位操作获取
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号

//通过宏获取
exitNormal = WIFEXITED(status);  //是否正常退出
exitCode = WEXITSTATUS(status);  //获取退出码
  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码。

6.3进程等待--wait函数

pid_t wait(int* status);
//等待任意子进程
//status:输出型参数,获取进程的退出状态,不关心可设置为NULL
//返回值:成功返回被等待的pid,失败返回-1
//wait函数
 int main()
 {
     int id = fork();
     if(id == 0)
     {
        //child
         int count = 10;
         while(count--)
         {
             printf("I am child... PID is: %d, PPID is : %d\n", getpid(), getppid());
             sleep(1);
         }
         exit(0); //子进程退出
     }

    //father

    int status = 0;
    pid_t ret = wait(&status);
    if(ret > 0)
    {
        //等待成功
        printf("wait success,the proccess is %d\n", ret);
        if(WIFEXITED(status)){
            printf("exit code is : %d\n", WEXITSTATUS(status));
        }
    }
    sleep(3);
    return 0;
}
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep;echo "######################";sleep 1;done

//脚本监控进程

image-20231214163834746

6.4waitpid函数

pid_t waitpid(pid_t pid, int* status, int options);
//作用:等待指定子进程或者任意子进程
//参数:
//1.pid:待等待子进程的pid,若设置为-1,则等待任意子进程。
//2.status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
//3.options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。
//返回值:等待成功返回被等待的pid
//如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
//如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
int main()
{
	pid_t id = fork();
	if (id == 0){
		//child          
		int count = 10;
		while (count--){
			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
		exit(0);
	}
	//father           
	int status = 0;
	pid_t ret = waitpid(id, &status, 0);
	if (ret >= 0){
		//wait success                    
		printf("wait child success...\n");
		if (WIFEXITED(status)){
			//exit normal                                 
			printf("exit code:%d\n", WEXITSTATUS(status));
		}
		else{
			//signal killed                              
			printf("killed by siganl %d\n", status & 0x7F);
		}
	}
	sleep(3);
	return 0;
}

image-20231214170630105

6.5非阻塞的轮询检测方案

我们已经了解过如何回收进程了,正常情况下,父进程可以一直阻塞式地等待子进程,但是我们肯定希望父进程能更高效,因此接下俩我们编写一个简单的非阻塞轮询检测的模型:

//非阻塞的轮询检测方案

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int count = 3;
        while(count--)
        {
            printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
            sleep(2);
        }
        //printf("bye bye\n");
        exit(0);
    }

    //father
    while(1)
    {
        int status = 0;
        //等待所有子进程,等待结束则返回pid,未结束时返回0,父进程则做自己的事
        pid_t ret = waitpid(id, &status, WNOHANG);
        if(ret > 0)
        {
            printf("wait success...\n");
            printf("exit code is %d\n", WEXITSTATUS(status));
            break;
        }
        else if(ret == 0)
        {
            printf("father do other thing\n");
            sleep(1);
        }
        else{
            printf("error !!!!\n");
            break;
        }
    }
    return 0;
}

image-20231214203550473

可以看到,在这种情况下,父进程可以先做别的事,然后再去等待子进程。

7.进程程序替换

7.1替换原理

用fork创建子进程后,子进程和父进程执行的是不同的代码,若想让子进程执行其他程序,往往需要调用exec函数进行程序替换。

当进程调用exec系列函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。

但是进行程序替换并不代表创建了新进程,进程对应的PCB、进程地址空间和页表等数据结果都没变,知识物理内存中的数据和代码发生了改变,pid也没有改变。

同样的,它不会影响父进程的代码和数据,因为当子进程程序替换时,就意味着要对子进程的数据和代码进行写入,此时就会发生写时拷贝,之后父子进程的代码数据也就分离了,不会有影响。

7.2替换函数

exce系列函数一共有6个,功能相同,但是参数不一致。

int execl(const char *path, const char *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 execve(const char *path, char *const argv[], char *const envp[]);

参数解释:

  • 1.path:表示要执行程序的路径
  • 2.arg:表示可变参数列表,即你要怎么执行,最后以NULL结尾
  • 3.envp[]:一个数组,里面是自己设置的环境变量
  • 4.argv[]:一个指针数组,数组当中的内容表示你要如何执行这个程序,最后以NULL结尾
  • 5.file:表示你要执行的程序的名字
  • 6.返回值:调用成功,则不再返回,加载指定的程序,失败返回-1

后缀含义:

  • l(list):表示参数采用列表一一列出
  • v(vector):表示参数采用数组形式
  • p(path):表示能自动搜索环境变量PATH,进行程序查找
  • e(env):表示可以传入自己设置的环境变量

实际上,这些函数最终都是调用execve,它才是真正的系统调用,其他函数都是为了满足用户不同需求而做的封装。

8.环境变量

8.1引入环境变量

在我们使用指令时,大家有没有想过为什么它们可以直接运行呢?而在Linux中为什么我们自己生成的可执行文件得带上“./”才可以呢?

其实很简单,想执行一个程序,必须要告诉OS它在哪里,“.”表示当前目录,所以我们自己的程序的程序得带上“./”,告诉OS他在当前目录下,这样指定在平时是最简单的方法,不可能每次执行都写上长长的地址。

//在Linux中,"."表示当前目录,".."表示上级目录,“~"表示主目录,即当前登录用户的用户目录,"/"则表示根目录

那我们使用指令的时候怎么办呢?其实OS已经提前将它们的一些运行参数导入了,这就是环境变量--即OS用来指定运行环境的一些参数。

环境变量一般都具有特殊用途,同时具有全局特征。

常见的环境变量:

  • PATH:指定命令的搜索路径。
  • HOME:指定用户的主工作目录(即我们平时登录普通用户时的目录)
  • SHELL:当前Shell,它的值一般是/bin/bash。
//查看环境变量
echo $NAME	
//name为待查看的环境变量名称

image-20231215160328189

这些环境变量都是以冒号结尾,在执行指令时,OS就会从这些路径中查找。当然我们也可以导入环境变量,本文就不再讲解了。

8.2再认识main函数

在系统中,环境变量通常是由一个指针数组维护的,里面每一个指针指向一个"\0"结尾的环境字符串,最后一个字符串为空,而每一个程序都会收到这张表,main函数也是。

main函数其实是由三个参数的,只是我们平时都不使用,就没有写出来了。

int main(int argc,char *argv[],char *envp[])
 //argc:参数的个数
 //argv[]:一个字符数组,第一个存储的是可执行程序的位置,剩下的是所给的若干选项,最后为空
 //envp[]:获取环境参数表

//测试前两个参数
int main(int argc, char *argv[], char *envp[])
{
    if(argc > 1)
    {
        if(strcmp(argv[1], "-a") == 0)
        {
            printf("you used -a option...\n");
        }
        else if(strcmp(argv[1], "-b") == 0)
        {
            printf("you used -b option...\n");
        }
        else
        {
            printf("you used -c option...\n");
        }
    }
    else{
        printf("请选择选项\n");
    }
    return 0;
}

image-20231215185402756

//测试第3个参数,打印前9个环境变量
int main(int argc, char *argv[], char *envp[])
{
    int i = 0;
    while(i <= 8)
    {
        printf("envp[%d] is %s\n",i, envp[i++]);
    }
    return 0;
}

image-20231215185631521

当然,除了以上方法,我们也可以通过系统调用函数--getenv函数来获取环境变量,这里就不再介绍了。