Linux系统编程——进程

目录

一、进程相关概念

1.1 什么是程序?什么是进程?有什么区别?

1.2 如何查看系统中有哪些进程?

1.2.1 使用ps指令查看:

1.2.2 使用top指令查看:

1.3 什么是进程标识符?

1.3.1 获取进程标识符函数gitpid函数原型和头文件:

1.3.2 获取自身的进程标识符案例:

1.4 什么是父进程?什么是子进程?

1.5 C程序的存储空间是如何分配的?

二、创建进程函数fork的使用

2.1 进程创建函数fork函数原型和头文件:

2.2 函数说明:

2.3 编程实现创建子进程 并且分别获取子进程和父进程的PID号:

2.4 根据fork函数的返回值也可以判断父子进程:

2.5 fork()父子进程的代码和数据的复制问题:

2.6 fork创建子进程的目的:

三、vfork函数创建进程

3.1 进程创建函数vfork函数原型和头文件:

3.2 vfork函数与fork函数的关键区别一:

3.3 vfork函数与fork函数的关键区别二:

四、进程退出

4.1 进程退出的三种情况:

4.2 进程退出码:

4.3 进程正常退出:

4.3.1 return退出:

4.3.2 exit函数退出:

4.3.3 _exit和 _Exit函数退出:

4.3.4 exit函数和_exit函数的区别:

4.4 进程异常退出:

五、父进程等待子进程退出

5.1 为什么父进程要等待子进程退出:

5.2 进程等待相关函数wait原型和头文件:

5.3 检查wait和waitpid所返回的终止状态的宏:

5.4 使用wait函数实现父进程等待子进程退出:

5.5 进程等待函数waitpid函数原型和头文件:

5.6 孤儿进程:

六、进程程序替换exec族函数

6.1 exec族函数作用和功能:

6.2 exec族函数原型和头文件:

6.3 exec族函数execl函数应用一:

6.4 exec族函数execl函数应用二:

6.5 exec族函数execl函数应用三(实现ls -l指令):

6.6 exec族函数execlp函数应用:

6.7 配置PATH环境变量:

6.8 exec族函数execv函数应用:

6.9 exec族函数execvp函数应用:

6.10 exec族函数配合fork函数应用:

七、system函数

7.1 system函数原型和头文件:

7.2 system函数应用:

八、popen函数

8.1 popen函数原型和头文件:

8.2 popen函数应用:


一、进程相关概念

  • 面试中关于进程,应该会问的的几个问题:

1.1 什么是程序?什么是进程?有什么区别?

  • 程序是静态的概念,比如:

  • 磁盘中生成的a.out文件,就叫做:程序

  • 进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程

  • 程序是静态的概念,进程是动态的概念

1.2 如何查看系统中有哪些进程?

1.2.1 使用ps指令查看:

  • ps指令显示的进程不够完整

  • ps -aux显示完整但是篇幅太长,不方便我们查看

  • 一般配合grep来查找程序中的某一个进程,例如查找初始化进程可以输入ps -aux|grep init回车

1.2.2 使用top指令查看:

  • 可以使用top指令查看,类似于windows的任务管理器

1.3 什么是进程标识符?

  • 进程标识符(process identifier,又略称为进程ID,或者PID)是大多数操作系统的内核用于唯一标识进程的一个数值。这一数值可以作为许多函数调用的参数,以使调整进程优先级、杀死进程之类的进程控制行为成为可能。

  • 在各 PID 中,较为特别的是 0 号 PID 和 1 号 PID。PID 为 0 者为交换进程(swapper),属于内核进程,负责分页任务;PID 为 1 者则常为 init 进程,主要负责启动与关闭系统。值得一提的是,1 号 PID 本来并非是特意为 init 进程预留的,而 init 进程之所以拥有这一 PID,则是因为 init 即是内核创建的第一个进程。不过,现今的许多 UNIX/类 UNIX 系统内核也有以进程形式存在的其他组成部分,而在这种情况下,1 号 PID 则仍为 init 进程保有,以与之前系统保持一致。

  • 每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证

1.3.1 获取进程标识符函数gitpid函数原型和头文件:
/*
	Linux下 man 2 gitpid查看手册
*/
#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

pid_t	获取到的进程标识符

getpid函数作用:获取自身的进程标识符
getppid函数作用: 获取父进程的进程标识符   
1.3.2 获取自身的进程标识符案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	pid_t pid;

	//pid_t getpid(void);
	pid = getpid();					//获取的自身进程标识符

	printf("my pid is %d\n",pid);	//输出获取的自身进程标识符

	while(1);
	return 0;
}

1.4 什么是父进程?什么是子进程?

  • 进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系

1.5 C程序的存储空间是如何分配的?

  • 参考《UNIX环境高级编程》的第七章:进程环境 7.6节:C程序的存储空间布局:

  • 正文段:又叫做:代码段,这是有CPU执行的机器指令部分。通常正文段是可以共享的,并且是只读的

  • 初始化数据段:通常将此段作为数据段,它包含了程序中需要明确的赋初值的变量,比如函数外的声明:int cnt = 10;

  • 非初始化数据段:通常此数据段称为bss段(block start symbol),在程序开始执行之前,内核将此段中的数据初始化为0或空指针。比如函数外声明:int arr[100];

  • :通常在堆中进行动态存储分配,由于历史上的惯例,堆位于非初始化数据段和栈之间

  • :自动变量以及每次函数调用所需保存的信息都存放在此段中。调用函数其返回地址也保存在栈中。递归函数每调用一次自身,就是用一个新的栈帧,这样一个函数调用中的变量集就不会影响另一个函数调用函数的变量

=========================================================================

=========================================================================

二、创建进程函数fork的使用

2.1 进程创建函数fork函数原型和头文件:

/*
	Linux下 man 2 fork查看手册
*/
 #include <unistd.h>

pid_t fork(void);

无参数     
pid_t		是一个宏定义,其实质是int 被定义在<sys/types.h>中
    
fork函数调用成功,返回两次
返回值为0		代表当前进程是子进程
返回值非负数	   代表当前进程为父进程
    
调用失败,返回-1

2.2 函数说明:

一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。 子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。 UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。

由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。

在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回,箭头表示各自的执行处。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。

引用一位网友的话来解释Pid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的Pid指向子进程的进程id,因为子进程没有子进程,所以其Pid为0。”

2.3 编程实现创建子进程 并且分别获取子进程和父进程的PID号:

根据父进程和子进程的pid不同的特点,我们可以在创建进程之前获取一次进程pid,这是父进程的pid,创建进程之后再一次获取进程pid,并通过判断两次pid是否相同判断哪个是父进程pid,哪个是子进程pid

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

int main()
{
    pid_t pid;
    pid_t pid2;

    pid = getpid();                     //获取fork之前进程PID
    printf("fork之前PID = %d\n",pid);

    fork();                             //创建一个子进程

    pid2 = getpid();                    //获取fork之后进程PID
    printf("fork之后PID = %d\n",pid2);

    if(pid == pid2){                    //如果pid == pid2代表是父进程
        printf("父进程PID\n");
    }else{                              //如果pid != pid2代表是子进程
        printf("子进程PID,子进程PID = %d\n",getpid());
    }
    
    return 0;
}

2.4 根据fork函数的返回值也可以判断父子进程:

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

int main()
{
    pid_t pid;
    pid_t pid2;
    pid_t retpid;

    pid = getpid();                     //获取fork之前进程PID
    printf("fork之前PID = %d\n",pid);

    retpid = fork();                    //创建一个子进程

    pid2 = getpid();                    //获取fork之后进程PID
    printf("fork之后PID = %d\n",pid2);

    if(pid == pid2){                    //如果pid == pid2代表是父进程
        printf("父进程PID,retpid = %d,父进程PID = %d\n",retpid,getpid());
    }else{                              //如果pid != pid2代表是子进程
        printf("子进程PID,retpid = %d,子进程PID = %d\n",retpid,getpid());
    }
}

  • fork之前只有一个父进程在运行,父进程的PID是54994,然后调用fork函数创建了一个子进程,fork调用成功后返回两次,两次返回唯一的区别是:子进程返回0值,父进程返回子进程PID是54995

2.5 fork()父子进程的代码和数据的复制问题:

进程数据=代码+数据 父进程创建子进程时,代码共享(因为代码在内存中一般为只读),数据私有(写时拷贝),这也就解释了上面的fork()为什么会有两个不同的返回值。

之前Linux系统采用完全拷贝,将父进程的内存地址和内容都重新拷贝一份 现在Linux系统采用写实拷贝只有再对某一变量运用时才执行拷贝 写实拷贝: 当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。 只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝技术。

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

int main()
{
    pid_t pid;
    int data = 10;

    printf("父进程PID = %d\n",getpid());    //获取父进程PID

    pid = fork();                           //创建一个子进程
    
    if(pid > 0){                            //返回值如果是非负数代表是父进程
        printf("PID > 0代表是父进程返回值 = %d,父进程PID = %d\n",pid,getpid()); //父进程的返回值是子进程的PID
    }else if(pid == 0){
        printf("PID = 0代表是子进程返回值 = %d,子进程PID = %d\n",pid,getpid()); //子进程的返回值是0
        data = data + 100;
    }

    printf("data = %d\n",data);
    return 0;
}

可以看到,data的值在子进程中改变时data = data +100;,是通过重新赋值了父进程的数据段修改的,父进程的data值没有改变

2.6 fork创建子进程的目的:

  1. 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达

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

int main()
{
    pid_t pid;
    int data;

    printf("父进程PID = %d\n",getpid());          //获取父进程PID

    while(1){
        printf("请输入一个数据:\n");           	//等待用户输入,当用户输入为1时父进程创建子进程,在子进程中处理请求。
        scanf("%d",&data);
        if(data == 1){
            pid = fork();                        //创建一个子进程

            if(pid > 0){                         //返回值如果是非负数代表是父进程
               
            }else if(pid == 0){
                while(1){
                     printf("网络请求!pid = %d\n",getpid()); //在子进程中执行网络操作
                     sleep(3);
                }           
            }
        }else{
            printf("什么都不做!\n");
        }
    }
    return 0;
}

2.一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

三、vfork函数创建进程

3.1 进程创建函数vfork函数原型和头文件:

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

pid_t vfork(void);

无参数     
pid_t		是一个宏定义,其实质是int 被定义在<sys/types.h>中
    
fork函数调用成功,返回两次
返回值为0		代表当前进程是子进程
返回值非负数	   代表当前进程为父进程   
调用失败,返回-1
    
vfork - 创建子进程并阻塞父进程

既然vfork函数也可以创建进程,与fork的区别是什么?

3.2 vfork函数与fork函数的关键区别一:

  • vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行

首先我们在fork的时候:

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

int main()
{
    pid_t pid;

    printf("父进程PID = %d\n",getpid());    //获取父进程PID

    pid = fork();                           //创建一个子进程
    
    if(pid == 0){							//返回值如果是0代表是子进程
        while(1){
            printf("PID = 0代表是子进程,子进程PID = %d\n",getpid());
            sleep(1);
        }   
    }else if(pid > 0){						//返回值如果是非负数整代表是父进程
        while(1){
            printf("PID > 0代表是父进程,父进程PID = %d\n",getpid());
            sleep(1);
        } 
    } 
    return 0;
}

首先我们在fork的时候:父进程和子进程同时运行!

然后我们在vfork的时候:

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

int main()
{
    pid_t pid;

    printf("父进程PID = %d\n",getpid());    //获取父进程PID

    pid = vfork();                          //创建一个子进程
    
    if(pid == 0){
        while(1){
            printf("PID = 0代表是子进程,子进程PID = %d\n",getpid());
            sleep(1);
        }   
    }else if(pid > 0){
        while(1){
            printf("PID > 0代表是父进程,父进程PID = %d\n",getpid());
            sleep(1);
        } 
    } 
    return 0;
}

我们在vfork的时候:当子进程不退出的时候,父进程无法运行

当改成让子进程执行三次退出后:

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

int main()
{
    pid_t pid;
    int cnt = 0;

    pid = vfork();

    if(pid == 0){                                   //当PID=0代表是子进程
        while(1){
            printf("子进程PID = %d\n",getpid());
            cnt++;
            if(cnt == 3){                           //当子进程执行三次后退出,退出之后执行父进程
                printf("子进程退出\n");
                exit(0);
            }
            sleep(1);
        }
    }else if(pid > 0){                              //当PID是一个非负整数代表是父进程
        while(1){
            printf("父进程PID = %d\n",getpid());
            sleep(1);
        }    
    }
    return 0;
}

我们可以看见当子进程正常退出之后,父进程才执行。

3.3 vfork函数与fork函数的关键区别二:

  • vfork直接使用父进程存储空间,与父进程共享数据段,不拷贝。

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

int main()
{
    pid_t pid;
    int cnt = 0;

    pid = vfork();

    if(pid == 0){                                   //当PID=0代表是子进程
        while(1){
            printf("子进程PID = %d\n",getpid());
            cnt++;
            if(cnt == 3){                           //当子进程执行三次后退出,退出之后执行父进程
                printf("子进程退出\n");
                exit(0);
            }
            sleep(1);
        }
    }else if(pid > 0){                              //当PID是一个非负整数代表是父进程
        while(1){
            printf("父进程PID = %d\n",getpid());
            printf("在父进程cnt = %d\n",cnt);
            sleep(1);
        }    
    }
    return 0;
}

可以发现当子进程改变cnt的值之后,父进程的cnt也在改变,因为改变的同一个cnt。

四、进程退出

4.1 进程退出的三种情况:

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

  2. 代码运行完毕,结果不正确

  3. 代码异常终止,进程崩溃

4.2 进程退出码:

main函数是间接性被操作系统所调用的。当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。 当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,可以使用echo $?命令查看最近一次进程退出的退出码信息。

比如:

代码正常运行结束后可以用echo $?命令查看退出码是0

 当代码被强行结束(ctrl+c)echo $?命令查看退出码是130

4.3 进程正常退出:

  • 从man函数返回,即调用return函数

  • 调用exit,标准C语言库

  • 调用_exit或者 _Exit,属于系统调用

  • 进程最后一个线程返回

  • 最后一个线程调用pthread_exit

  • 最后一个线程对取消(cancellation)请求做出响应

4.3.1 return退出:

在main函数中使用return退出是我们常用的方法

#include <stdio.h>

int main()
{
    printf("Hello World\n");
    return 0;
}

4.3.2 exit函数退出:
/*
	exit函数原型和头文件
*/
#include <stdlib.h>

void exit(int status);

exit函数说明:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构。exit(0)表示正常退出,exit(x)(x不为0)表示异常退出,这个x是返回给操作系统(包括UNIX,Linux,和MS DOS)的,以供其他程序使用。

4.3.3 _exit和 _Exit函数退出:
/*
	_exit和_Exit函数函数原型和头文件
*/

#include <unistd.h>

void _exit(int status);

#include <stdlib.h>

void _Exit(int status);

函数说明

_exit函数会立即终止调用过程。属于该进程的任何打开的文件描述符都被关闭;进程的任何子进程都由init进程(初始化进程,进程ID:1)继承,进程的父进程将被发送一个SIGCHLD信号。值状态作为进程的退出状态返回给父进程,并且可以使用wait(2)系列调用之一收集。

_Exit函数等效于 _exit函数。

4.3.4 exit函数和_exit函数的区别:
  • exit()函数定义在stdlib.h中,而_exit()定义在unistd.h中

  • 最大的区别就在于exit()函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是"清理I/O缓冲"

exit()在结束调用它的进程之前,要进行如下步骤:

  1. 调用atexit()注册的函数(出口函数),按ATEXIT注册时相反的顺序调用所有由它注册的函数,这使得我们可以指定在程序终止时执行自己的清理动作。例如,保存程序状态信息于某个文件,解开对共享数据库上的锁等。

  2. cleanup()关闭所有打开的流,这将导致写所有被缓冲的输出,删除用TMPFILE函数建立的所有临时文件。

  3. 最后调用_exit()函数终止进程。

_exit在结束调用它的进程之前,要进行如下步骤:

  1. 关闭属于该进程的所有打开的文件描述符。

  2. 进程的任何子进程都由init进程继承。

  3. 向进程的父进程发送SIGCHLD信号。

4.4 进程异常退出:

  • 调用abort

  • 由信号终止,如ctrl+c

五、父进程等待子进程退出

5.1 为什么父进程要等待子进程退出:

  • 父进程等待子进程退出并收集子进程退出状态,如果子进程退出状态不被收集,那么子进程会变成僵尸进程。

  • 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

在之前我们使用vfork创建子进程的时候,子进程退出时没有被父进程收集其退出的状态,因此子进程最终会变成“僵尸进程”。

5.2 进程等待相关函数wait原型和头文件:

/*
	Linux下man 2 wait查看手册
*/

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);

pit_t			函数返回值,等待成功返回被等待进程的PID,等待失败则返回-1
int *wstatus	输出型参数,获取子进程的退出状态(传入的是整型指针),不关心可设置为NULL。
  • 父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:

  1. 阻塞等待子进程退出

  2. 回收子进程残留资源

  3. 获取子进程结束状态(退出原因)

  • wait一旦被调用,就会一直阻塞在这里,直到有一个子进程退出出现为止。

  • 调用成功,则清理掉的子进程ID,失败则返回-1,表示没有子进程。

  • 使用wait函数传出参数status来保存进程的退出状态(正常终止→退出值;异常终止→终止信号)。

借助宏函数来进一步判断进程终止的具体原因。

5.3 检查wait和waitpid所返回的终止状态的宏:

1.WIFEXITED(status) 为非0 → 进程正常结束

WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)

2.WIFSIGNALED(status) 为非0 → 进程异常终止

WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。

3.WIFSTOPPED(status) 为非0 → 进程处于暂停状态

WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。

WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行

5.4 使用wait函数实现父进程等待子进程退出:

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

int main()
{
    pid_t pid;
    int cnt = 0;

    pid = fork();

    if(pid == 0){                                   //当PID=0代表是子进程
        while(1){
            printf("子进程PID = %d\n",getpid());
            cnt++;
            if(cnt == 5){                           //当子进程执行三次后退出,退出之后执行父进程
                printf("子进程退出\n");
                exit(0);
            }
            sleep(1);
        }
    }else if(pid > 0){                              //当PID是一个非负整数代表是父进程
        while(1){
            wait(NULL);                             //不关心子进程状态时用NULL
            printf("父进程PID = %d\n",getpid());
            printf("在父进程cnt = %d\n",cnt);
            sleep(1);
        }    
    }
    return 0;
}

子进程(进程ID:79930)执行完后被清除,没有变成僵尸进程。

如果需要子进程退出时的状态,可以在exit函数写入一个状态值,例如向父进程返回一个3,可以写成exit(3),父进程则需要用到宏函数WEXITSTATUS(status)获取状态值。

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

int main()
{
    pid_t pid;
    int cnt = 0;
    int status = 10;

    pid = fork();

    if(pid == 0){                                   //当PID=0代表是子进程
        while(1){
            printf("子进程PID = %d\n",getpid());
            cnt++;
            if(cnt == 5){                           //当子进程执行三次后退出,退出之后执行父进程
                printf("子进程退出\n");
                exit(3);
            }
            sleep(1);
        }
    }else if(pid > 0){                              //当PID是一个非负整数代表是父进程
        while(1){
            wait(&status);                                                  //阻塞父进程,等待子进程退出
            printf("子进程退出,子进程status = %d\n",WEXITSTATUS(status));      //获取子进程的结束状态
            printf("父进程PID = %d\n",getpid());
            printf("在父进程cnt = %d\n",cnt);
            sleep(1);
        }    
    }
    return 0;
}

5.5 进程等待函数waitpid函数原型和头文件:

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *wstatus, int options);	//等待指定子进程或任意子进程

pit_t			函数返回值:
1.等待成功返回被等待进程的pid
2.如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
3.如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
    
pid_t pid	
1.pid = -1		等待任一子进程,此种情况下:wait和waitpid等效
2.pid >  0		等待进程ID与pid相等的子进程
3.pid =  0		等待组ID等于调用进程组ID的任一子进程
4.pid < -1		等待组ID等于pid绝对值的任一子进程
    
int *wstatus	输出型参数,获取子进程的退出状态,不关心可设置为NULL
int options		提供了一些额外的选项来控制waitpid    
常量:		  说明:
WCONTINUED	若实现支持作业控制,那么由pid 指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态(POSIX.1的XSI扩展)
WNOHANG		若由pid指定的子进程并不是立即可用的,则 waitpid不阻塞,此时其返回值为 0
WUNTRACED	若某实现支持作业控制,而由 pid 指定的任一子进程已处于停止状态,并且其状态自停止以来还未报告过,则返回其状态。				WIESTOPPED宏确定返回值是否对应于一个停止的子进程

wait和waitpid的区别:

  1. wait 使调用者阻塞(子进程不结束,就一直不会运行父进程)

  2. waitpid 有一个选项可以使调用者不阻塞

5.6 孤儿进程:

  • 父进程如果不等待子进程的退出,在子进程之前就“结束”了自己的生命,此时子进程叫孤儿进程。

  • Linux 避免系统存在过多的孤儿进程,init 进程收留孤儿进程,变成孤儿进程的父进程。

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

int main()
{
    pid_t retpid;
    int cnt = 0;

    retpid = fork();                                   //创建一个子进程

    if(retpid == 0){                                   //当PID=0代表是子进程
        while(1){
            printf("子进程PID = %d,父进程PID = %d\n",getpid(),getppid());
            cnt++;
            if(cnt == 3){                           //当子进程执行三次后退出,退出之后执行父进程
                printf("子进程退出\n");
                exit(0);
            }
            sleep(1);
        }
    }else if(retpid > 0){                              //当PID是一个非负整数代表是父进程    
        printf("父进程PID = %d\n",getpid());       
    }
    return 0;
}
#include <unistd.h>

extern char **environ;

int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

六、进程程序替换exec族函数

6.1 exec族函数作用和功能:

  • exec族函数作用:用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。

  • exec族函数功能:在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件

6.2 exec族函数原型和头文件:

#include <unistd.h>

extern char **environ;

int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
  • 函数返回值:exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行

  • pathname:要执行的程序路径。可以是绝对路径或者是相对路径。在execv、execve、execl和execle这4个函数中,使用带路径名的文件名作为参数

  • file:要执行的程序名称。如果该参数中包含“/”字符,则视为路径名直接执行;否则视为单独的文件名,系统将根据PATH环境变量指定的路径顺序搜索指定的文件

  • arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束

  • argv:命令行参数的矢量数组

  • envp:带有该参数的exec函数可以在调用时指定一个环境变量数组。其他不带该参数的exec函数则使用调用进程的环境变量

  • . . .:命令行参数列表。调用相应程序时有多少命令行参数,就需要有多少个输入参数项。注意:在使用此类函数时,在所有命令行参数的最后应该增加一个空的参数项(NULL),表明命令行参数结束

exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:

l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量

6.3 exec族函数execl函数应用一:

/*demo17.c文件*/
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("execl之前:\n");
    if(execl("./echoarg","echoarg","abc",NULL) == -1){
        printf("execl失败\n");
    }
    printf("execl之后\n");
    return 0;
}
/*echoarg.c文件*/
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("execl之前:\n");
    if(execl("./echoarg","echoarg","abc",NULL) == -1){  //如果函数返回值为-1代表调用execl函数失败,反之直接进入														      echoarg程序执行
        printf("execl失败\n");

        perror("why");                                      //调用perror函数输出:出错信息
    }
    printf("execl之后\n");
    return 0;
}
gcc echoarg.c -o echoarg
shiyahao@shiyahao-virtual-machine:~/LinuxJincheng$ ./echoarg

6.4 exec族函数execl函数应用二:

  • 我们直接在Linux下输入date指令会显示出系统的时间,但是我们想使用代码来获取系统的时间,所以使用execl函数实现

 首先我们得知道date的路径:

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

int main()
{
    printf("execl之前:\n");
    if(execl("/bin/date","date",NULL,NULL) == -1){      //如果函数返回值为-1代表调用execl函数失败,反之获取系统时间
        printf("execl失败\n");

        perror("why");                                      //调用perror函数输出:出错信息
    }
    printf("execl之后\n");
    return 0;
}

6.5 exec族函数execl函数应用三(实现ls -l指令):

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

int main()
{
    printf("execl之前:\n");
    if(execl("/bin/ls","ls","-l",NULL) == -1){      //如果函数返回值为-1代表调用execl函数失败,反之执行ls -l指令
        printf("execl失败\n");

        perror("why");                                      //调用perror函数输出:出错信息
    }
    printf("execl之后\n");
    return 0;
}

6.6 exec族函数execlp函数应用:

我们会发现我们每一次运行另一个程序都需要用whereis来查找路径,如果不写路径会怎样?

 很显然execl找不到date的文件路径,我们使用execlp函数就可以解决这个问题.

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

int main()
{
    printf("execl之前:\n");
    if(execlp("date","date",NULL,NULL) == -1){      //如果函数返回值为-1代表调用execlp函数失败,反之直接获取系统时间
        printf("execl失败\n");

        perror("why");                                      //调用perror函数输出:出错信息
    }
    printf("execl之后\n");
    return 0;
}

  • 使用execlp函数来获取系统时间就可以不用加绝对路径了,因为execlp函数能通过环境变量PATH查找可执行文件date

6.7 配置PATH环境变量:

echo $PATH		//输出当前环境变量指令

如果需要修改环境变量可以在后面进行追加,比如想将:/etc/apache2/bin添加为环境变量,可以这样写:

export PATH=$PATH:/etc/apache2/bin		//自己配置环境变量用export指令

我们在运行程序的时候在程序名之前就不需要加./了,也可以运行其他路径下的可执行程序

6.8 exec族函数execv函数应用:

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

int main()
{
    char *argv[] = {"ls","-l",NULL};	//把所有的参数全部都放到数组中

    printf("execl之前:\n");
    if(execv("/bin/ls",argv) == -1){	//如果函数返回值为-1代表调用execl函数失败,反之执行ls -l指令
        printf("execl失败\n");

        perror("why");					//调用perror函数输出:出错信息
    }
    printf("execl之后\n");
    return 0;
}

6.9 exec族函数execvp函数应用:

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

int main()
{
    char *argv[] = {"date",NULL,NULL};

    printf("execl之前:\n");
    if(execvp("date",argv) == -1){      //如果函数返回值为-1代表调用execl函数失败,反之直接进入echoarg程序执行
        printf("execl失败\n");

        perror("why");                                      //调用perror函数输出:出错信息
    }
    printf("execl之后\n");
    return 0;
}

6.10 exec族函数配合fork函数应用:

当用户输入1时创建子进程修改配置文件的字段值,当前配置文件LENG=9,通过 exec族函数配合fork函数修改成5

/*file_peizhi.c*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv)
{
	int fdSrc;

	char *readBuf = NULL;
	
	if(argc != 2){											//判断C文件参数是不是有两个,如果不是程序退出
		printf("param error\n");
		exit(-1);
	}

	fdSrc = open(argv[1],O_RDWR);							//打开配置文件
	
	int size = lseek(fdSrc,0, SEEK_END);					//计算配置文件有多少个字节
	lseek(fdSrc, 0, SEEK_SET);								//让配置文件光标回到头
	
	readBuf = (char *)malloc(sizeof(char) * size + 8);		//动态开辟readBuf的内存空间

	int n_read = read(fdSrc,readBuf,size);					//把配置文件的size个字节的内容读取到readBuf里面

	//char *strstr(const char *haystack, const char *needle);
	char *p = strstr(readBuf,"LENG=");				//字符串查找函数,返回值为要查找的字符串的第一个字符的指														      针,第一个参数为待查找的原始字符串,第二个参数为要查找的内容
	p = p + strlen("LENG=");									//偏移LENG的长度,偏移到数据位置
	*p = '5';													//更改数据位置的值

	lseek(fdSrc, 0, SEEK_SET);									//让配置文件光标回到头

	int n_write = write(fdSrc,readBuf,strlen(readBuf));			//把读出的内容重新写入配置文件

	close(fdSrc);												//关闭配置文件
	return 0;
}

/*demo24.c*/
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;
    int data;

    printf("父进程PID = %d\n",getpid());            //获取父进程PID

    while(1){
        printf("请输入一个数据:\n");           //等待用户输入,当用户输入为1时父进程创建子进程,在子进程中处理请求。
        scanf("%d",&data);
        if(data == 1){
            pid = fork();                           //创建一个子进程

            if(pid > 0){                            //返回值如果是非负数代表是父进程
               wait(NULL);                          //父进程等待子进程退出
            }else if(pid == 0){
                while(1){
                    if(execl("./file_peizhi","file_peizhi","config.txt",NULL) == -1){	//执行已经写好的配置文件
                        printf("execl失败\n");
                        perror("why");
                    }
                }           
            }
        }else{
            printf("什么都不做!\n");
        }
    }
    return 0;
}

可以看到成功把配置文件config.txt中的LENG=9改成LENG=5

七、system函数

  • system是一个C/C++的函数,Linux操作系统下system 函数主要是执行shell 命令

7.1 system函数原型和头文件:

#include <stdlib.h>

int system(const char *command);

int 	函数返回值
1. 成功则返回进程的状态值
2. 当 sh 不能执行时。返回127
3. 其他原因失败返回-1
  • 函数说明:system函数会调用fork函数产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。在调用system函数期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。

  • 不同于exec族函数,system执行完之后还会执行原来的程序

7.2 system函数应用:

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

int main()
{
    printf("execl之前:\n");
    if(system("date") == -1){      //如果函数返回值为-1代表调用system函数失败,反之获取系统时间
        printf("execl失败\n");

        perror("why");             //调用perror函数输出:出错信息
    }
    printf("execl之后\n");
    return 0;
}

八、popen函数

  • popen函数允许一个程序将另外一个程序作为新进程来启动,并可以传递数据或者通过它接受数据。其内部实现为调用 fork 产生一个子进程,执行一个 shell, 以运行命令来开启一个进程,这个进程必须由 pclose 函数关闭。

  • popen函数与system函数在应用中的好处是可以获取运行的输出结果

8.1 popen函数原型和头文件:

#include <stdio.h>

FILE *popen(const char *command, const char *type);

int pclose(FILE *stream);

FILE *					返回值是一个文件指针,函数执行成功返回文件指针,否则返回NULL,可用来存储执行后的结果
const char *command		是一个指向以NULL结束的shell命令字符串指针,shell将执行的命令   
const char *type		
1. "r":文件指针连接到 command 的标准输出
2. "w":文件指针连接到 command 的标准输入    
由于popen是以创建管道的方式创建进程连接到子进程的标准输出设备或标准输入设备,因此其带有管道的一些特性,同一时刻只能定义为写或者读。

8.2 popen函数应用:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    FILE* fp;
    int fd;
    char readBuf[1024] = {0};
    //FILE *popen(const char *command, const char *type);

    int size = sizeof(readBuf) / sizeof(readBuf[0]);            //计算数组的大小
    fp = popen("ps","r");                                       //运行ps指令

    //size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    int n_read = fread(readBuf,1,size,fp);                      //从fp文件流里面每次读1bit读size次到readBuf里面

    printf("read ret = %d,readBuf = \n%s\n",n_read,readBuf);    //read ret = 读取的字节数,然后输出readBuf的内容

    fd = open("./file",O_RDWR|O_CREAT,0600);                    //可读可写方式打开file文件,如果没有则创建它
    //ssize_t write(int fd, const void *buf, size_t count);
    int n_write = write(fd,readBuf,size);                       //把readBud中的内容写size个字节到file文件中
    printf("通过write函数向file文件写入了%d个字节的数据\n",n_write);

    pclose(fp);                                                 //关闭文件流
    close(fd);                                                  //关闭file文件
    return 0;
}

  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值