Linux复习之进程控制

目录

一.fork函数

二.写时拷贝

 为什么要有写时拷贝

进程退出码

程序异常退出

进程退出的方法

进程等待

进程等待的方法

wait

waitpid 

status的构成

进程等待的意义

进程的程序替换


一.fork函数

一个非常重要的函数,在已有的进程中创建一个新进程。新进程为子进程,原进程为父进程。

#include <unistd.h>

pid_t fork(void);

返回值: 子进程中返回0,父进程返回子进程id ,出错返回 -1

当进程调用fork时,当控制会转移到内核中的fork代码中后,内核中的fork中实现细节:

  也就是说在fork中return的前面,子进程就已经创建完成了,所以到return那里,子进程和父进程分别返回不同的pid(因为父进程跟子进程的代码是共享的,所以它们会执行相同的代码)。 

所以, fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行。注意, fork之后,谁先执行完全由调度器决定。

二.写时拷贝

概念:

当父进程创建子进程的时候,系统不会立马给子进程创建一块空间,而是让子进程中的数据先指向父进程的数据。当父子进程中有一个要进行写入数据的时候,才创建一定的空间给那个要写入数据的进程,这就是写时拷贝。

 父进程创建一个子进程时候:

1.系统会创建相对应的task_struct(进程控制块),mm_struct(进程地址空间),页表等数据结构给子进程。

2.然后父进程会将task_struct(进程控制块),mm_struct(进程地址空间),页表中的大部分数据拷贝给子进程中相对应的task_struct(进程控制块),mm_struct(进程地址空间),页表,也就是说,刚创建子进程的时候,子进程中的数据和代码与父进程都是共享的。包括·数据的虚拟地址都是一样的。所谓的共享就是在虚拟内存中映射到物理内存是相同的一块空间。如下图所示:    

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    int a = 10;
    pid_t id = fork();
    if (id == 0)
    {
        // child
        printf("child: a: %d &a: %p\n", a, &a);
        sleep(1);
        exit(1);
    }
    else
    {
        printf("parent: a: %d &a: %p\n", a, &a);
        sleep(1);
    }

    return 0;
}

 创建的时候,如果不对数据进行修改,子进程和父进程的的数据是一样的,其中a的地址也是一样,这是虚拟地址,是父进程拷贝给子进程的。

3.当父子进程之间其中有一个要对数据段进行写入的时候,系统会开辟一块的物理空间大小给要写入的进程,然后修改页表指向的物理空间,这就是写时拷贝。例如父子进程的a的值指向的同一块物理空间,当子进程要去修改a时,系统会在物理内存中创建一块跟a一样大小的空间给a,然后将子进程的页表指向新的物理空间,然子进程去进行修改。
 

a发生了改变, 我们看到父进程的a是10,子进程的a是1000.此时就发生了写时拷贝。同时它们的的地址都是相同的,这是虚拟地址,虚拟地址映射到物理地址是不相同的。

 

 为什么要有写时拷贝

1.进程具有独立性,父子进程也是,当父子进程其中一个进程要写入数据的时候,是不能影响到另一个进程。

2.当子进程创建出来的时候不会立马分配空间给子进程,这可以节约系统内存的空间,因为子进程创建出来的时候,可能不会立马被cpu给调度,所以系统可以合理去利用这些空间去做更有意义的事情,当子进程需要多少空间的时候,才分配多少空间给子进程。
 

进程退出码

进程退出的场景:

1.代码运行完毕,结果正确。

2.代码运行完毕,结果不正确,例如:进程申请空间失败,或者打开文件失败,创建子进程失败等原因导致运行结果错误。

我们一般不关心代码运行完毕,结果正确这种场景。因为运行成功则只有一种情况。而代码运行结束,结果不正确,我们往往是最关心的,因为结果不正确的原因有很多种,我们得知道具体失败的原因,好让我们去维护

当运行完一个程序的时候,我们要查看该程序是否运行正确,我们可以通过查看进程的退出码去判断运行该程序是否正确。如果正确,那么会返回一个我们在main函数中的return的值(默认是0),或者exit中的值。如果程序运行的不正确,那么可以根据退出码去判断运行结果错误的类型。

下面我们将程序运行成功时的进程退出码设置为1:main函数return 后面就是进程退出码

 int main()
 {
   printf("hello Linux\n");
  return 1;                                                                                                                             
 }

运行结束时,在查看进程退出码。 echo $?:显示最近的进程退出码 

如果进程失败的话,那么它会进程退出码将不是return返回的值,而是其他非0的数字,不同非0的数字代表的是运行结果错误的类型。

程序异常退出

进程代码还么没跑完就提前退出了,这种情况叫异常退出

例如,ctrl+c,或者指针越界都会导致程序提前退出。

提前退出的的进程退出码我们不关心。我们需要关心的是进程收到什么信号。

kill -l可以查看各种退出信号。

进程退出的方法

正常退出

1.main函数的return

int main
{
     printf("hello Linux\n");                  
    return 0;                                                                                                  
     printf("hello world\n");                                                                                                            
 }

 当进程遇到在main函数的时候遇到return的时候,进程就一样退出了,不会在执行以下代码,所以hello world不会被打印出来,如果进程没有遇到错误,则返回的是进程后面的值。(编译不会通过,理解就好)

2.调用_exit

void test()
 {          
   printf("hello test\n");                                                          
  _exit(1);                                           
 }     
 
int main()
{
   test();
    printf("hello linux\n");
   return 10;
}

 当进程遇到_exit时就退出,如果运行成功,则返回括号里面的退出码。并且不论是在哪里,则进程都会退出,而return只在main函数里面才会退出。

 3.调用exit.

exit跟_exit一样,进程不论在哪个位置遇到,则直接退出进程。

exit 最后也会调用 _exit, 但在调用 exit 之前,还做了其他工作:
1. 执行用户通过 atexit 或 on_exit 定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用 _exit

 说人话就是exit清理I/O缓冲区后再退出进程。

异常退出

ctrl+ c

进程等待

等待分为阻塞等待非阻塞等待

阻塞等待:如果条件不满足的话,则会一直等,不会做任何事情。

非阻塞等待:也是等,并不会因为条件不满足,而“卡住”,它会继续忙着自己的事情。

进程终止了,操作系统做了什么?

操作系统需要释放掉进程的pcb,mm_struct,想对应的页表,代码和数据申请的空间也被释放掉,回收资源,各种队列中移除该进程的数据结构,其中,要释放掉进程pcb之前,需要先等待父进程的来读取退出信息,才能够被释放掉。

进程等待的方法

wait

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值 : 成功返回被等待进程pid ,失败返回 -1 。
参数 : 输出型参数,获取子进程退出状态, 不关心则可以设置成为 NULL
 

父进程通过等待的方式,回收子进程的资源,获取子进程退出信息。

返回值 :当正常返回的时候 wait 返回收集到的子进程的进程 ID;如果调用中出错 , 则返回 -1。

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // child
        printf("i am child process.ppid:%d pid:%d\n", getppid(), getpid());
        sleep(1);
    }
    else
    {
        int status = 0;
        wait(&status);
        printf("i am parent process. ppid: %d pid: %d\n", getppid(), getpid());
        sleep(1);
    }
    return 0;
}

当父进程运行到wait时,父进程一直在等子进程退出,然后将子进程的退出状态传给status,当子进程退出之前,父进程就一直在那里等,不会做任何事情,直到子进程结束后,才会运行接下来的代码。

waitpid 

pid_ t waitpid (pid_t pid,  int *status,  int options );

返回值: 成功返回被等待进程pid,失败返回-1

pid:传要等待进程的pid,传-1 ,等待任意一个子进程。

status:下面详解,

options:

阻塞进程之间传0即可。

传WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0,不予以等待。若正常结束,则返回该子进程的 ID 。

可以非阻塞式等待
 

status的构成

status是一个int型的数字,他不能简单看作整形来看待,而是应该用位图来看待,它是有32个bit位,但是我们只关心它的低16个bit位,高16个bit位我们不关心。

当进程正常退出的时候:status的8到15位代表的是进程退出码,0到7位是进程的终止信号,此时是没收到任何信号,所以为0.

 

也就是说status的8到15位代表的是进程退出码,0到7位是进程的终止信号。

所以进程退出码=(status>>8)&0xFF

进程的终止信号=(status&0f7F); 

上面是status的基本组成,但是我们一般不用

((status>>8)&0xFF)去获得进程退出码

 或者用status&0xFF去判断进程是否异常。

我们有两个接口:

WIFEXITED(status) : 若为正常终止子进程返回的状态,则为真,如果异常退出为即假。(查看进程是否是正常退出 )
WEXITSTATUS(status) : 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
 

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        printf("child ppid=%d,pid=%d",getppid(),getpid());
    }
    else
    {
        int status=0;
        wait(&status);
        if(WIFEXITED(status))
        {
            printf("father ppid=%d pid=%d",getppid(),getpid());
            printf("进程退出码:%d\n",WEXITSTATUS(status));
        }
    }
    return 0;
}

进程等待的意义

首先,子进程退出,父进程如果不管不顾,就可能造成 ‘ 僵尸进程 ’的问题,进而造成内存泄漏,其中的子进程的task_struct还没有被释放掉。然后, 父进程派给子进程的任务完成的怎样,我们需要知道。子进程运行完成后,结果对还是不对, 或者是否正常退出。所以进程等待是为了释放子进程的空间,和读取子进程的退出信息。
 

进程的程序替换

 用 fork 创建子进程后执行的是和 父进程相同 的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动 例程开始执行。调用exec 并不创建新进程 , 所以调用 exec前后该进程的id并未改变 。

#include <unistd.h>`
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[]);

 

这些函数我们只需要记住exec,然后在记住后面加的字符代表的含义即可:

l(list) : 表示参数采用列表,执行的方式用参数一个一个的传进去,最后注意传一个NULL
v(vector) : 参数用数组,先把参数放在数组里,后把数组传给函数即可
p(path) : 有 p 自动搜索环境变量 PATH,直接传文件名,不需要传路径
e(env) : 表示自己维护环境变量。

 enevp可以将自己写的环境变量替换掉默认的环境变量。

  getenv:获取进程环境变量的内容,如果没有则,返回(null);头文件为stdlib.h

事实上 , 只有 execve 是真正的系统调用 , 其它五个函数最终都调用 execve, 所以 execve 在 man 手册 第 2 节 , 其它函数在 man手册第 3 节。这些函数之间的关系如下图所示。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值