Linux系统编程-进程

Linux进程

一、进程相关概念

1、进程定义

狭义定义:进程是正在运行的程序的实例

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

  • 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
  • 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)数据区域(data region)堆栈区(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

在这里插入图片描述

2、进程的特性

  • **动态性:**进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的;

  • **并发性:**任何进程都可以同其他进程一起并发执行;

  • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;

  • **异步性:**由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。

结构特征:进程由程序、数据和进程控制块三部分组成。

多个不同的进程可以包含相同的程序:一个程序在不同的程序集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

3、进程、线程与程序

  1. 程序:程序并不能单独执行,是静止的,只有将程序加载到内存中,系统为其分配资源后才能够执行。

  2. 进程:程序对一个数据集的动态执行过程,一个进程包含一个或者更多的线程,一个线程同时只能被一个进程所拥有,进程是分配资源的基本单位。进程拥有独立的内存单元,而多个线程共享内存,从而提高了应用程序的运行效率。

  3. 线程:线程是进程内的基本调度单位,线程的划分尺度小于进程,并发性更高,线程本身不拥有系统资源,但是该线程可与同属进其他线程共享该进程所拥有的全部资源。每一个独立的线程,都有一个程序运行的入口、顺序执行序列和程序的出口。

在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器

  • 进程和线程的区别

    1.一个进程可以包含至少一个线程,一般来说也就是主线程,而一个线程只能属于一个进程;

    2.进程拥有独立的内存,而线程没有独立的资源空间, 只是暂时存储在计数器,寄存器,栈中,同一个进程间的线程可以共享资源;

    3.将代码放入到代码区之后,进程产生,但还没执行,我们所说的执行一般是是主线程main函数开始执行;

    4.进程比线程更加消耗资源;

    5.进程对资源的保护要求高,而线程要求不高;

    6.进程是处理器这一层面的抽象,而线程是进程的基础上进一步并发的抽象;

    7.同一个进程下,一个线程的挂掉,会导致整个进程的挂掉,而进程之间不会相互影响。

  • 进程和程序的区别

    1.程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态的概念;

    2.程序可以作为一种软件资料长期存在,而进程是有一定生命期的。程序是永久的,进程是暂时的;

    3.进程更能真实地描述并发,而程序不能;

    4.进程具有创建其他进程的功能,而程序没有;

    5.同一程序同时运行于若干个数据集合上,它将属于若干个不同的进程,也就是说同一程序可以对应多个进程;

    6.在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单元都是进程。

4、进程的状态

  1. 就绪状态(Ready):

进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。

  1. 运行状态(Running):

进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

  1. 阻塞状态(Blocked):

由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。
在这里插入图片描述

5、Linux环境下查看系统中的进程

  1. 使用ps aux指令查看所有的进程,在实际工作中可以配合grep查找相关进程:ps aux|grep 进程名;

  2. 使用top指令查看,类似windows任务管理器。

二、进程控制

1、进程标识:pid

每个进程都有一个非负整型表示的唯一进程ID。虽然是唯一的,但是进程ID是可复用的:当一个进程终止后,其进程ID就成为复用的候选者。大多数系统会使用延迟复用算法,使得赋予新建进程ID不同于最近进程所使用的ID,这防止了将新进程误认为是使用同一ID的某个已终止 的先前进程。

系统中有一些专用进程:

  • ID为0的进程通常是调度进程,常被称为交换进程(swapper)。该进程是内核的一部分,不执行任何磁盘上的程序,因此也被称为系统教程。
  • ID为1的进程通常是init进程,在自举过程结束时由内核调用。init通常读取与系统有关的初始化文件,并将系统引导到一个状态(如多用户)。init进程绝不会终止,他是一个普通的用户进程(与交换进程不同,他不是内核中的系统进程),但是它以超级用户特权运行。下文部分会说明init如何成为所有孤儿进程的父进程

除了进程ID,每个进程还有一些其他标识符:

#include<unistd.h>
pid_t getpid(void);//返回值:调用进程的进程ID
pid_t getppid(void);//返回值:调用进程的父进程ID

pid_t getuid(void);//返回值:调用进程的实际用户ID
pid_t geteuid(void);//返回值:调用进程的有效用户ID

pid_t getgid(void);//返回值:调用进程的实际组ID
pid_t getegid(void);//返回值:调用进程的有效组ID

2、创建进程

A进程如果创建了B进程,A进程就是B进程的父进程,B进程就是A进程的子进程。

(1)fork函数

在Linux中,我们通常使用fork函数来为一个已经存在的进程创建一个新进程。而这个新创建出来的进程被称为原进程的子进程,原进程被称为该进程的父进程。

#include <unistd.h>
pid_t fork(void);
/*fork函数调用成功,返回两次:子进程返回值为0,父进程返回子进程pid(非负数),代表当前进程为父进程; 
调用失败,返回-1*/

fork在创建一个进程后,子进程和父进程继续执行fork调用之后的指令,子进程是父进程的副本。子进程会复制父进程的PCB,二者之间代码共享,数据独有,拥有各自的进程虚拟地址空间。

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

int main()
{
    
    
    pid_t pid;
    pid = getpid();
    fork();
    printf("my pid = %d,curent pro id = %d\n",pid,getpid());
    return 0;
}
/*运行结果
my pid = 6569,curent pro id = 6569
my pid = 6569,curent pro id = 6570*/

既然代码共享,并且子进程是拷贝了父进程的PCB,虽然他们各自拥有自己的进程虚拟地址空间,但其中的数据必然是相同的(拷贝而来),并且通过页表映射到同一块物理内存中,那么又如何做到数据独有呢?答案是:通过写时拷贝技术。

写时拷贝技术:

  • 内核只为新生成的子进程创建虚拟空间结构,它们来复制父进程的虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的物理空间:

在这里插入图片描述

  • 当父子进程任意一方要对数据进行修改时,都可能会对另一方造成影响,上面又说到任意进程之间是具有独立性的,不会互相影响,那么这时操作系统就会介入,**会给父进程和子进程重新在物理内存中开辟一块空间,并将数据拷贝过去。父子进程也不再指向原来的那一份数据,而是指向修改拷贝的这一份数据。**这就叫做写实拷贝。这样避免了直接给子进程重新开辟内存空间,造成内存数据冗余。

在这里插入图片描述

下面我们来观察一下数据在父子进程中改变后的现象:

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

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

    printf("data = %d, data adrress = %p\n",data,&data);
    pid = fork();
    if(pid < 0){
    
    
        printf("fork error!\n");
        return -1;
    }
    if(pid == 0){
    
    
        data = data + 100;
        printf("chiled pid = %d, chiled data = %d, data adrress 
                = %p\n",getpid(),data,&data);
    }
    if(pid > 0){
    
    
        data = data + 300;
        printf("father pid = %d, father data = %d, data adrress 
                = %p\n",getpid(),data,&data);
    }
    return 0;
}
/*运行结果
data = 10, data adrress = 0x7fff1a8d9878
father pid = 8542, father data = 310, data adrress = 0x7fff1a8d9878
chiled pid = 8543, chiled data = 110, data adrress = 0x7fff1a8d9878*/
观察结果发现,虽然data数据在父子进程中都发生了改变,但是对应的虚拟地址都是相同的。

子进程拷贝父进程的PCB,拥有和父进程一模一样的进程虚拟地空间以及数据,但父进程和子进程将自己
的data更改后,会在物理内存中为其重新开辟空间来存储进程更改后的数据,而结果中看到的地址完全相
同,则是因为它们仅仅是虚拟的地址空间,真正的值是存储在物理内存中的。而这时通过页表的映射,这
俩个看似相同的地址已经指向了不同的物理内存。

fork函数有以下两种用法:

  1. 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的-父进程等待客户端的服务请求,当这种请求到达时,父进程调用fork,使子进程处理此请求,父进程则继续等待下一个服务请求。
  2. 一个进程要执行一个不同的程序。这对shell是常见的情况,在这种情况下,子进程从fork返回后立即调用exec。
(2)vfork函数

除了fork函数,vfork也同样是用来创建子进程的系统调用函数,并且返回值及其含义和fork也相同。

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);

vfork和fork的区别:

vfork和fork同样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit,于是不会引用复制的虚拟空间,但是在子进程调用exec或exit之前,它直接在父进程的空间中运行。另外,vfork会保证子进程先运行,在子进程调用exec或exit之后父进程才可能被调度运行,当子进程调用这exec或exit任意一个时,父进程会恢复运行。即:

  • vfork 直接使用父进程存储空间,不拷贝。
  • vfork保证子进程先运行,当子进程调用exec或exit退出后,父进程才执行。
#include <stdio.h>
#include <unistd.h>

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

    printf("data = %d, data adrress = %p\n",data,&data);
    pid = vfork();
    if(pid < 0){
    
    
        printf("fork error!\n");
        return -1;
    }
    if(pid == 0){
    
    
        sleep(3);
        data = data + 100;
        printf("chiled pid = %d, chiled data = %d, data adrress  
                 = %p\n",getpid(),data,&data);
        _exit(0);
    }
    if(pid > 0){
    
    
        data = data + 300;
        printf("father pid = %d, father data = %d, data adrress 
                = %p\n",getpid(),data,&data);
    }
    return 0;
}
/*运行结果
data = 10, data adrress = 0x7fffece5c438
chiled pid = 9283, chiled data = 110, data adrress = 0x7fffece5c438
father pid = 9282, father data = 410, data adrress = 0x7fffece5c438*/
观察结果发现:
    在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法,但在
    vfork中虽然子进程使用sleep函数使子进程休眠三秒,但依然是子进程先输出结果,然后父进程才
    打印结果,即vfork会保证子进程先执行。
    data数据在子进程中更改之后,父进程使用的data数据是子进程修改过的值,即子进程直接在父进程
    的存储空间。

3、进程终止

有八种方式使进程终止,其中有五种为正常终止:

  • Main函数调用return;
  • 进程调用exit(),标准c库;
  • 进程调用_exit() 或者 _Exit(),属于系统调用;
  • 进程最后一个线程返回;
  • 最后一个线程调用pthread_exit。

有三种异常终止:

  • 调用abort;
  • 当进程收到某些信号时,如ctrl+C;
  • 最后一个线程对取消(cancellation)请求做出响应。

​ 不管进程如何终止,最后都会执行内核中的一段代码。这段代码为相应进程关闭所有打开描述符,释放他所使用的存储器。对于上述任意一种终止详情,我们都希望终止进程能够通知其父进程它是如何终止的。对于3个终止函数(exit、_exit和 _Exit),实现这一点的方法是,将**其退出状态(exit status)作为参数传送给函数。**在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(temmination status)。在任意一种情况下,该终止进程的父进程都能用 wait 或waitpid 函数(将在下一节说明)取得其终止状态。

#include <unistd.h>
void _exit(int status);

#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

/*使用wati将父进程先挂起,等待子进程执行完后调用终止函数,然后父进程才继续执行。*/
int main()
{
    
           
    pid_t pid;
    int cnt = 0;
    int data = 10;

    pid = fork();
    if(pid < 0){
    
    
        printf("fork error!\n");
        return -1;
    }
    if(pid == 0){
    
    
        while(1){
    
    
            cnt++;
            printf("chiled pid = %d, cnt = %d\n",getpid(),cnt);
            if(cnt == 3){
    
    
                _exit(0);
                //exit(0);
                //_Exit(0);
            }
        }
    }
    if(pid > 0){
    
    
        wait(NULL);
        printf("father pid = %d\n",getpid());
    }
    return 0;
}
/*运行结果
chiled pid = 9749, cnt = 1
chiled pid = 9749, cnt = 2
chiled pid = 9749, cnt = 3
father pid = 9748*/

4、等待子进程退出

  • 为什么要等待子进程退出?

我们创建子进程是为了让子进程帮父进程干活,那活干的到底怎么样、有没有完成,父进程需要收到一个反馈来判断下一步该如何执行。

  • 如果不收集子进程的退出状态会怎么样?

父进程如果不收集子进程的退出状态,那么子进程可能会变成僵死进程(也叫僵尸进程)。

  • 父进程不等待子进程退出就直接终止会怎么样?

父进程如果不等待子进程退出,在子进程结束之前就结束了自己的“生命”,此时子进程叫做孤儿进程。Linux避免系统存在过多的孤儿进程,init进程会收留孤儿进程,变成孤儿进程的父进程。

  • 如何等待子进程退出?

调用wait 或者 waitpid函数,子进程调用exit退出后会返回一个状态码,然后使用wait和waitpid所返回终止状态的宏来解析状态码,从而确定子进程的状态。

(1)僵尸进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

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

    pid = fork();
    if(pid < 0){
    
    
        printf("fork error!\n");
        return -1;
    }
    if(pid == 0){
    
    
        while(1){
    
    
            cnt++;
            printf("chiled pid = %d, cnt = %d\n",getpid(),cnt);
            if(cnt == 3){
    
    
                _exit(0);
            }
        }
    }
    if(pid > 0){
    
    
        while(1){
    
    
            //wait(NULL);
            printf("father pid = %d\n",getpid());
            sleep(3);
        }
    }
    return 0;
}   

运行结果如下:我们不用wait收集子进程的退出状态,通过查看子进程的状态直接变成了Z+(Zombie)僵尸进程。
在这里插入图片描述

(2)孤儿进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    
    
    pid_t pid;

    int cnt = 0;
    int data = 10;
    int status = 10;
    pid = fork();
    if(pid < 0){
    
    
        printf("fork error!\n");
        return -1;
    }
    if(pid == 0){
    
    
        while(1){
    
    
            cnt++;
            printf("chiled pid = %d, my father pid = %d\n",getpid(),getppid());
            sleep(1);
            if(cnt == 4){
    
    
                exit(1);
            }
        }
    }
    if(pid > 0){
    
    
        printf("father pid = %d\n",getpid());
        sleep(1);
    }
    return 0;
}
/*运行结果
father pid = 11505
chiled pid = 11506, my father pid = 11505
chiled pid = 11506, my father pid = 1
chiled pid = 11506, my father pid = 1
chiled pid = 11506, my father pid = 1*/
/*观察结果可以看到父进程在子进程之前终止,此时子进程叫做孤儿进程。Linux避免系统存在过多的孤儿
进程,init进程会收留孤儿进程,变成孤儿进程的父进程。*/
(3)等待子进程退出
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
/*两个函数的返回值:
调用成功,返回进程ID;
调用失败,测返回0或-1*/

/*status参数:是一个整型数指针
非空:子进程退出状态放在它所指向的地址中;
空:不关心退出状态。*/

有四个互斥的宏可以用来取得进程终止的原因:
在这里插入图片描述

从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。

两个函数的区别如下:

  • 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞;
  • wait等待第一个终止的子进程,而waitpid则可以指定等待特定的子进程;
  • waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。
1.wait使用实例
  • 如果其所有子进程都还在运行,则阻塞;
  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回;
  • 如果它没有任何子进程,则立即出错返回。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    
    
    pid_t pid;

    int cnt = 0;
    int data = 10;
    int status = 10;
    pid = fork();
    if(pid < 0){
    
    
        printf("fork error!\n");
        return -1;
    }
    if(pid == 0){
    
    
        while(1){
    
    
            cnt++;
            printf("chiled pid = %d, cnt = %d\n",getpid(),cnt);
            if(cnt == 3){
    
    
                _exit(3);
            }
        }
    }
    if(pid > 0){
    
    
       	wait(&status);
        while(1){
    
    
            //子进程正常终止,使用WIFEXITSTATUS宏解析_exit传递的值
            printf("father pid = %d,status = %d\n",getpid(),WEXITSTATUS(status));
            sleep(3);
        }
    }
    return 0;
}
/*运行结果
chiled pid = 10571, cnt = 1
chiled pid = 10571, cnt = 2
chiled pid = 10571, cnt = 3
father pid = 10570,status = 3
father pid = 10570,status = 3
.............................*/
/*在子进程结束之前父进程一直处于阻塞状态,等待子进程正常退出后,父进程收集子进程的退出状态并
开始执行父进程。*/
2.waitpid使用实例

waitpid函数提供了wait函数没有提供的三个功能。

  • waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态;
  • waitpid提供了一个wait的非阻塞版本,有时希望获取一个子进程的状态但不想阻塞;
  • waitpid通过WUNTRACED和WCONTINUED选项支持作业控制。

waitpid函数中pid参数的作用解释如下:
在这里插入图片描述

options参数使我们能进一步控制waitpid的操作。此参数或者是0,或者是下图中常量按位或运算的结果:
在这里插入图片描述

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

int main
{
    
    
    pid_t pid;

    int cnt = 0;
    int data = 10;
    int status = 10;
    pid = fork();
    if(pid < 0){
    
    
        printf("fork error!\n");
        return -1;
    }
    if(pid == 0){
    
    
        while(1){
    
    
            cnt++;
            printf("chiled pid = %d, cnt = %d\n",getpid(),cnt);
            sleep(2);
            if(cnt == 5){
    
    
                exit(1);
            }
        }
    }
    if(pid > 0){
    
    
        //获取子进程的状态但不阻塞;
        waitpid(pid,&status,WNOHANG);
        while(1){
    
    
            printf("father pid = %d\n",getpid());
            sleep(1);
        }
    }
    return 0;
}

运行结果:父进程获得子进程的运行状态,并且不发生阻塞:

在这里插入图片描述

5、exec族函数

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

在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe

#include <unistd.h>
extern char **environ;
//函数原型:
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 execvpe(const char *file, char *const argv[],char *const envp[]);
  1. 返回值:exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。

  2. 参数说明:

    • path:可执行文件的路径名字;
    • arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束;
    • file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
  3. exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:

    • l : 使用参数列表;
    • p:使用文件名,并从PATH环境进行寻找可执行文件;
    • v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数;
    • e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量。
  4. 功能:在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

下面将exec函数归为带l、带p、带v、带e 四类来说明参数特点。

(1)execl函数

带l的一类exec函数(l表示list),包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。
以execl函数为例子来说明:

//文件echoarg.c
#include <stdio.h>

int main(int argc,char *argv[])
{
    
    
    int i = 0;
    for(i = 0; i < argc; i++)
    {
    
    
        printf("argv[%d]: %s\n",i,argv[i]); 
    }
    return 0;
}

先观察一下如果execl函数中的文件路径错误是什么结果:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    
    
    printf("before execl\n");
    if(execl("/bin/echoarg","echoarg","abc",NULL) == -1){
    
    
        printf("execl failed\n");
        perror("why");//使用perror打印失败原因
    }
    printf("after execl\n");
    return 0;
}
/*运行结果:
before execl
execl failed
why: No such file or directory
after execl*/

//echoarg文件不在bin目录下,直接执行execl函数会调用失败,然后从原程序的调用点接着往下执行。

将execl函数中的文件路径修改正确:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    
    
    printf("before execl\n");
    //可执行路径名,可执行文件名,参数列表,参数列表必须以NULL结尾
    if(execl("./echoarg","echoarg","abc",NULL) == -1){
    
    
        printf("execl failed\n");
        perror("why");//使用perror打印失败原因
    }
    printf("after execl\n");
    return 0;
}
/*运行结果
before execl
argv[0]: echo
argv[1]: abc*/
//程序如果调用成功不会从原程序继续执行,而是执行新的可执行文件

实验说明:我们先用gcc编译echoarg.c,生成可执行文件echoarg并放在当前路径目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。

知道了execl的函数用法,下面使用execl函数打印一下系统时间:

/*先使用whereis date找到date指令的目录
CLC@Embed_Learn:~/LinuxProcess$ whereis date
date: /bin/date /usr/share/man/man1/date.1.gz*/

//找到目录后使用execl调用date指令打印系统时间
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    
    
    printf("get system date:\n");
    //这里直接调用date指令,不用传参
    if(execl("/bin/date","date",NULL,NULL) == -1){
    
    
        printf("execl failed\n");
        perror("why");
    }
    printf("after execl\n");
    return 0;
}
/*运行结果
get system date:
Tue May 21 10:45:55 CST 2024*/
(2)ececlp函数

带p的一类exac函数,包括execlp、execvp、execvpe,如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

上述execl中,想要找到可执行文件必须加入可执行文件的路径名才能调用execl函数,如果使用execlp直接用可执行文件名代替路径:**通过环境变量PATH搜寻可执行文件。**下面使用execlp打印一下系统时间观察区别:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    
    
     printf("get system date:\n");
    //使用execlp在环境变量中查找可执行文件date
    if(execlp("date","date",NULL,NULL) == -1){
    
    
        printf("execl failed\n");
        perror("why");
    }
    printf("after execlp\n");
    return 0;
}
/*运行结果
get system date:
Tue May 21 10:55:53 CST 2024*/
(3)execvp函数

带v不带l的一类exac函数,包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。如char *arg[]这种形式,且arg最后一个元素必须是NULL,例如char *arg[] = {“date”,NULL,NULL};

下面以execvp函数为例说明:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
    
    
    printf("get system date:\n");
    //构造指针数组指向函数的参数
    char *argv[] = {
    
    "date",NULL,NULL};
    if(execvp("date",argv) == -1){
    
    
        printf("execl failed\n");
        perror("why");
    }
    printf("after execvp\n");
    return 0;
}
/*运行结果
get system date:
Tue May 21 11:10:05 CST 2024*/
(4)execle函数

带e的一类exac函数,包括execle、execvpe,可以传递一个指向环境字符串指针数组的指针。 参数例如char *env_init[] = {“AA=aa”,”BB=bb”,NULL}; 带e表示该函数取envp[]数组,而不使用当前环境。

下面以execle函数为例:

//文件echoenv.c
#include <stdio.h>
#include <unistd.h>
extern char** environ;
int main(int argc , char *argv[])
{
    
    
    int i;
    char **ptr;
    for(ptr = environ;*ptr != 0; ptr++)
        printf("%s\n",*ptr);
    return 0;
}
//先写一个显示全部环境表的程序,命名为echoenv.c
//文件execle.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execle(const char *path, const char *arg,..., char * const envp[]);

char *env_init[] = {
    
    "AA=aa","BB=bb",NULL};
int main(void)
{
    
    
    printf("before execle****\n");
        if(execle("./bin/echoenv","echoenv",NULL,env_init) == -1)
        {
    
    
                printf("execle failed!\n");
        }       
    printf("after execle*****\n");
    return 0;
}
/*运行结果
ubuntu:~/test/exec_test$ gcc execle.c -o execle
ubuntu:~/test/exec_test$ ./execle
before execle****
AA=aa
BB=bb*/

实验说明:先写一个显示全部环境表的程序,命名为echoenv.c,然后编译成可执行文件放到./bin目录下。然后再运行可执行文件execle,发现我们设置的环境变量确实有传进来。

(5)exec配合fork使用

使用exec族函数调用changeData可执行文件更改配置文件中的值:

//文件changeData.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    
    
    int fdSrc;
    char *readBuf = NULL;

    fdSrc = open("config.txt",O_RDWR);
    int size = lseek(fdSrc,0,SEEK_END);
    lseek(fdSrc,0,SEEK_SET);

    readBuf = (char *)malloc(sizeof(char)*size + 8);
    int n_read = read(fdSrc,readBuf,size);
    char *p = strstr(readBuf,"LENG=");
    if(p == NULL){
    
    
        printf("not found\n");
        exit(-1);
    }
    p = p + strlen("LENG=");
    *p ='9';

    lseek(fdSrc,0,SEEK_SET);
    int n_write = write(fdSrc,readBuf,strlen(readBuf));
    close(fdSrc);
    return 0;
}
/*先生成一个changeData的可执行文件,该文件的作用是打开配置文件config.txt,将其中的LENG的值
更改成为9*/
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    
    
    pid_t pid;
    int data = 0;
    while(1){
    
    
        printf("please input a data:\n");
        scanf("%d",&data);
        if(data == 1){
    
    
            pid = fork();
            if(pid > 0){
    
    
                wait(NULL);
            }
            if(pid == 0){
    
    
                execl("./changeData","changeData","config.txt",NULL);
                printf("change success\n");
            }
        }
        else{
    
    
            printf("wait, do nothing\n");
        }
    }
    return 0;
}
/*使用execl调用changeData可执行文件,然后更改配置文件中的内容*/

运行结果:子进程使用execl函数调用了changeData可执行文件,然执行changeData中的主函数,将配置文件config.txt中LENG的值从5改成了9。
在这里插入图片描述

6、system函数

#include <stdlib.h>
int system(const char *command);
/*函数的返回值:
成功,则返回进程的状态值;若参数string为空指针(NULL),则返回非零值;
当shell不能执行时,返回127;
失败返回-1。*/

system()函数说明:
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。

system源码分析:

int system(const char * cmdstring)
{
    
    
	pid_t pid;
	int status;
//当system接受的命令为NULL时直接返回
	if(cmdstring == NULL){
    
     
		return (1);
	}
  //fork出一个子进程,因为fork在两个进程:父进程和子进程中都返回,这里要检查返回的 pid
	if((pid = fork())<0){
    
    
		status = -1;
	}
  //fork在子进程中返回0
	else if(pid == 0){
    
    
        //子进程调用execl来启动一个shell代替自己:
        //shell的路径是/bin/sh,后面的字符串都是参数,然后子进程就变成了一个 shell进程,
		//这个shell的参数是cmdstring,就是system接受的参数,(char *)0则是NULL。
		execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        -exit(127); //子进程正常执行则不会执行此语句
    }
	else{
    
    
        //在父进程中返回子进程的pid,父进程使用waitpid等待子进程结束
		while(waitpid(pid, &status, 0) < 0){
    
    
			if(errno != EINTER){
    
    
            status = -1;
            break;
			}
        }
    }
    return status;
}

system使用实例:同样调用exec实例中的changeData可执行文件来改变config.txt中LENG的值

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

int main()
{
    
    
    pid_t pid;
    int data = 0;
    while(1){
    
    
        printf("please input a data:\n");
        scanf("%d",&data);
        if(data == 1){
    
    
            pid = fork();
            if(pid > 0){
    
    
                wait(NULL);
            }
            if(pid == 0){
    
    
                system("./changeData");
                printf("change success\n");
            }
        }
        else{
    
    
            printf("wait, do nothing\n");
        }
    }
    return 0;
}

运行结果:观察发现,使用system函数也成功将config.txt中LENG的值改成了9,不同于exec函数的是,system执行完changeData后会继续执行system原来程序的函数,打印了change success。
在这里插入图片描述

7、popen函数

popen() 函数用于创建一个管道:其内部实现为调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程,这个进程必须由 pclose() 函数关闭。由于管道是单向的,所以参数类型只能指定为只读或者只写,返回的结果(标准I/O流)也是只读或者只写。

#include <stdio.h>
FILE *popen(const char *command, const char *type);
/*返回值:如果调用成功,则返回一个读或者打开文件的指针,如果失败,返回NULL,具体错误要根据
errno判断*/

参数说明:

  • command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。
  • mode: 只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。
int pclose(FILE *stream); 
//返回值:调用失败返回-1
  • stream:popen返回的文件指针

popen函数使用实例:调用ls指令显示当前路径下的所有文件

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

//FILE *popen(const char *command, const char *type);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

int main()
{
    
    	
    //定义一个文件流fp
    FILE *fp;
    //初始化ret大小为1024个字节
    char ret[1024] = {
    
    0};
	
    // 用fp接收popen执行后的结果
    fp = popen("ls","r");
    //将fp中的内容读取到ret中
    int n_read = fread(ret,1,1024,fp);
    //打印popen的执行结果
    printf("raed ret = %d byte\nret = %s",n_read,ret);
    //关闭popen调用fork产生的子进程
    pclose(fp);
    return 0;
}

运行结果:
在这里插入图片描述

8、exec族函数、system和popen的区别

exec族函数、system()函数和popen()函数都与执行外部命令或程序有关,但它们在用途和用法上有一些重要的区别。

exec族函数:

  • 用途:exec函数族用于在当前进程中执行其他程序,通常是替代当前进程的映像。主要用于在同一进程内启动新程序,替换当前进程的代码和数据。
  • 包括函数:exec函数族包括一系列函数,如execl、execlp、execve等,用于不同的参数传递方式。
  • 用法:exec函数族通常需要在父进程中先调用fork()创建一个子进程,然后在子进程中使用exec函数执行其他程序。这样可以保留父进程的状态并在子进程中切换到新的程序。

system()函数:

  • 用途:system()函数用于执行系统命令或外部程序,它启动一个新的shell进程,然后在该shell中执行指定的命令。主要用于执行简单的系统命令或外部程序。
  • 使用:system()函数非常简单,只需传递要执行的命令字符串即可。它不需要调用fork()或exec(),并且返回命令的退出状态。

popen()函数:

  • 用途:popen()函数用于打开一个管道并与外部命令建立双向通信。它可以用于在C程序中与外部命令进行输入和输出交互,类似于管道。
  • 使用:popen()函数返回一个文件指针,可以用于向外部命令发送输入并从命令接收输出。它允许在C程序中与外部命令进行复杂的通信,而不仅仅是简单地执行命令。

总结:
如果你需要在同一进程中执行其他程序,通常使用exec函数族,结合fork来创建子进程。
如果你只需要执行系统命令或外部程序,并不需要与其进行输入/输出交互,可以使用system()函数。
如果需要与外部命令进行输入和输出交互,可以使用popen()函数。

猜你喜欢

转载自blog.csdn.net/dearzhangxp/article/details/139100626