Linux 系统调用 fork wait exec

参考资料

1. 一些Unix/Linux进程相关词汇

  • 进程上下文:

    进程的上下文就是外界给进程提供的运行环境,即程序正确运行所需的状态组合。当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。

  • 上下文切换:

    不同任务(进程)之间的执行切换机制称为上下文切换,将一个任务的执行环境更改为另一个任务的执行环境。

  • 并发:

    逻辑上的同时进行。如果上下文切换的够快,就会给人一种同时执行多个任务的错觉。这种逻辑并行性称为并发。

  • 并行:

    物理上的同时进行(真正的同时进行)。对于多CPU或处理器内核的多处理器系统中,可在不同CPU上实时、并行的执行多项任务。

  • 执行映像:

    包含代码、数据和堆栈的存储区。程序和进程之间不存在一一对应的关系,一个程序可以对应多个的进程。程序是静态的,存储在计算机磁盘中,进程是动态的,存在生命周期,有“生老病死”。我们通常把进程看作为是程序在内存中的镜像image。

  • 进程:

    进程是对映像的执行(也可以理解为一个执行中程序的实例)。每个进程用一个独特的数据结构表示,即为进程控制块(PCB),PCB保存着该进程的所有信息。

2. Linux中进程相关系统调用

2.1 fork()系统调用

用法:

#include<unistd.h>
#include<sys/types.h>
int pid = fork();

百度百科定义:复刻(英语:fork,又译作派生分支)是UNIX或类UNIX中的分叉函数,fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

fork()创建子进程并返回子进程的pid(进程识别号),如果失败返回-1。父进程调用fork()系统调用后陷入内核态,并创建拥有自己的内核态堆栈和用户态映像的子进程。创建的子进程拥有与父进程完全相同的用户态映像,因此可以认为子进程就是父进程的一个复刻(因此连执行到哪一行都一样)

并且,允许子进程继承父进程打开的所有文件。因此,父进程和子进程都可以从同一个stdin标准输入文件获得输入,并输出到同一个stdout标准输出文件、stderr标准错误文件(可以理解为从同一个终端获取输入与显示输出)

在创建完子进程即fork()函数执行完后,parent父进程获得子进程pid号作为返回值,而刚创建的子进程会返回到相同的语句,获得的返回值为0

示例:父进程创建子进程后,两个进程打印fork()函数返回值并显示在相同终端上

main.c

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main(){
    printf("test begin\n");
    int pid = fork();
    if(pid){
        //父进程执行此部分
        printf("this is parent process : return value = %d\n",pid);
    }
    else{
        //子进程执行此部分
        printf("this is child process : return value = %d\n",pid);
    }
}

运行结果:

xtark@xtark-vmpc:~/桌面/linux_study/section3$ gcc main.c 
xtark@xtark-vmpc:~/桌面/linux_study/section3$ ./a.out 
test begin
this is parent process : return value = 3360
this is child process : return value = 0

可见父进程成功创建子进程并获得返回值3336(子进程pid),而子进程获得的fork()函数返回值为0。并且"test begin"只被打印了一次,因此说明fork函数将运行着的程序分成2个(几乎)完全一样的进程,连执行到哪一行都一样。父子进程能够将字符串输出到同一终端,说明子进程继承了父进程打开的所有文件。

2.2 getpid()和getppid()系统调用

这两个系统调用很简单,getpid()系统调用返回调用进程的pid,getppid()系统调用返回父进程的pid(ppid即为parent pid)。

示例:父进程创建子进程后,父进程打印自己与子进程pid;子进程打印自己与父进程pid

main.c

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main(){
    int pid = fork();
    if(pid){
        //父进程执行此部分
        printf("my pid is %d , my child pid is %d\n",getpid(),pid);
    }
    else{
        //子进程执行此部分
        printf("my pid is %d , my parent pid is %d\n",getpid(),getppid());
    }
}

运行结果:

xtark@xtark-vmpc:~/桌面/linux_study/section3$ gcc main.c 
xtark@xtark-vmpc:~/桌面/linux_study/section3$ ./a.out 
my pid is 3528 , my child pid is 3529
my pid is 3529 , my parent pid is 3528

2.3 wait()系统调用

用法

#include <sys/types.h>
#include <sys/wait.h>
int pid = wait(int * status);

父进程一旦调用了wait()系统调用就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回该子进程pid,并且status保存着僵尸子进程的exitCode(用于记录进程退出状态);如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

示例:父进程创建子进程后进行wait()系统调用,销毁该子进程并打印输出。子进程被创建后sleep五秒,之后到达程序末尾成为僵尸子进程。

main.c

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/wait.h>
int main(){
    int pid = fork();
    if(pid){
        //父进程执行此部分
        printf("I'm parent process,I create child process %d.\n",pid);
        int status;//保存子进程退出状态
        pid = wait(&status);//等待僵尸子进程出现并销毁该子进程
        printf("child process %d died\n",pid);
    }
    else{
        //子进程执行此部分
        printf("I'm child process %d\n",getpid());
        sleep(5);//令当前进程暂停5秒
        printf("five seconds left\n");
    }
}

运行结果:

xtark@xtark-vmpc:~/桌面/linux_study/section3$ gcc main.c 
xtark@xtark-vmpc:~/桌面/linux_study/section3$ ./a.out 
I'm parent process,I create child process 3904.
I'm child process 3904
five seconds left
child process 3904 died

2.4 exec系列系统调用

exec系列中的系统调用都完成相同的功能(只是参数不同而已),即为更改进程执行映像。它们把一个新程序装入调用进程的内存空间,来改变调用进程的执行映像。如果exec调用成功,调用进程将被覆盖,然后从新程序的入口开始执行,但是它的pid与调用进程相同(即为exec系统调用不改变继承的pid)。这就是说,exec没有建立一个与调用进程并发的新进程,而是用新进程取代了原来的进程。所以,在exec调用成功后,没有任何数据返回,执行失败返回-1。下面给出了exec系列系统调用在linux系统库unistd.h中的函数声明:

int execl(const char *path,const char *arg,...);
int execlp(const char *file,const char *arg,...);
int execle(const char *path,const chr *arg,...,char * const envp[]);
int execv(const char *path,char * const argv[]);
int execvp(const char *file,char * const argv[]);

2.4.1 获取系统环境变量

环境变量通过env[]参数传递给C程序,该参数是一个以NULL结尾的字符串指针数组,每个指针指向一个环境变量字符串

#include <stdio.h>
int main(int argc,char* argv[],char* env[]){
    int index = 0;
    while(env[index]){
        printf("%s\n",env[index++]);
    }
}

运行结果(环境变量太多了只截了一小部分)

xtark@xtark-vmpc:~/桌面/linux_study/section3$ gcc showEnv.c 
xtark@xtark-vmpc:~/桌面/linux_study/section3$ ./a.out 
LC_PAPER=zh_CN.UTF-8
XDG_VTNR=7

2.4.2 通过execl()更改进程映像

execl()函数格式:

第一个参数path是可执行文件路径。第二个以及用省略号表示的其他参数一起组成了该程序执行时的参数表,按照linux的贯例,参数表的第一项是不带路径的程序文件名。被调用的程序可以访问这个参数表,它们相当于shell下的命令行参数(即为传递给了main函数的char* argv[]参数)。

int execl(const char *path,const char *arg,...);

exec常用法:exec系统调用经常与fork()联合使用,我们可以先用fork建立一个子进程,然后在子进程中使用exec,这样就实现了父进程运行一个与其不同的子进程,并且父进程不会被覆盖。

示例一:父进程fork一个子进程,子进程以另一个可执行文件更改自己的执行映像。

main.c

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main(){
    int pid = fork();
    if(pid){
        //父进程执行此部分
        printf("I'm parent process,I create child process %d.\n",pid);
    }
    else{
        //子进程执行此部分
        printf("I'm child process %d\n",getpid());
        int r = execl("another","another","0","1",NULL);//更改进程映像
        printf("change Process image failed\n");
    }
}

another.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc,char* argv[]){
    printf("I changed myself to a new Process image , my pid is %d\n",getpid());
    printf("paremeters:\n");
    for(int i = 0 ; i < argc ; ++i){
        printf("%d paremter is : %s\n",i,argv[i]);
    }
}

运行结果:

xtark@xtark-vmpc:~/桌面/linux_study/section3$ gcc another.c -o another
xtark@xtark-vmpc:~/桌面/linux_study/section3$ gcc main.c 
xtark@xtark-vmpc:~/桌面/linux_study/section3$ ./a.out 
I'm parent process,I create child process 5731.
I'm child process 5731
xtark@xtark-vmpc:~/桌面/linux_study/section3$ I changed myself to a new Process image , my pid is 5731
paremeters:
0 paremter is : another
1 paremter is : 0
2 paremter is : 1

通过输出结果可以看出,更改进程映像后进程pid没有变化,但是运行的代码变成了another.c中的内容

自然也可以通过exec系统调用来运行linux命令如ls。因为ls本质上也是一个可执行文件,保存在/bin/目录下。

示例二:通过execl函数更改进程映像从而执行linux中的ls命令,需要注意传递的第二个及之后的参数相当于shell下的命令行参数,并且要以NULL结尾。

callCommand.c

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main(){
    printf("this is a Process\n");
    int r = execl("/bin/ls","ls","-l",NULL);//更改进程映像,执行命令。命令为ls -l
    printf("change to new Process image failed\n");
}

运行结果

xtark@xtark-vmpc:~/桌面/linux_study/section3$ gcc callCommand.c 
xtark@xtark-vmpc:~/桌面/linux_study/section3$ ./a.out 
this is a Process
总用量 44
-rwxrwxr-x 1 xtark xtark 8712 6月  13 22:27 another
-rw-rw-r-- 1 xtark xtark  300 6月  13 22:26 another.c
-rwxrwxr-x 1 xtark xtark 8664 6月  13 22:45 a.out
-rw-rw-r-- 1 xtark xtark  240 6月  13 22:45 callCommand.c
drwxrwxr-x 2 xtark xtark 4096 6月  13 21:35 fork test
-rw-rw-r-- 1 xtark xtark  477 6月  13 22:40 main.c
-rw-rw-r-- 1 xtark xtark  150 6月  13 21:29 showEnv.c
上一篇:rest-assured + testng + moco使用


下一篇:cisp-pte靶场通关思路分享----综合题篇