【Linux系统编程之进程】创建进程,父进程等待子进程退出,fork,vfork,exec族函数等

进程相关概念

什么是程序,什么是进程

程序

  • 程序是静态的代码集合,用来完成特定的任务。程序本身是存储在磁盘上的文件,它包括源代码、编译后的二进制文件等。
  • 程序是静态的,指在没有执行的状态下

进程:

  • 进程是程序的一次执行实例,它是一个动态的实体。进程不仅包括程序代码,还包括当前活动中的程序计数器、寄存器内容、变量、堆栈和程序数据等。

  • 进程是操作系统资源分配的基本单位,每个进程都有独立的内存空间,操作系统通过进程来管理程序的运行

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

区别:静态与动态:程序是静态的代码,进程是程序运行时的动态实体。
存储位置:程序通常存储在磁盘上,进程运行在内存中。
生命周期:程序没有生命周期概念,进程有从创建、执行到终止的生命周期

查看系统中的进程

  • 使用 ps 命令,如 ps -aux 查看所有进程的详细信息。
  • 使用 top 或 htop 命令实时监控系统的进程。
  • 使用 pstree 命令以树形结构显示进程。

进程标识符(Process Identifier, PID):

  • 进程标识符是操作系统分配给每个进程的唯一整数标识,用来区分不同的进程。

  • PID在操作系统中是唯一的,操作系统通过PID来进行进程管理和调度。

  • 可以通过系统命令或编程接口获取进程的PID,例如在Linux中使用 ps 命令查看PID,在Windows中使用 tasklist 命令

    Pid=0: 称为交换进程(swapper)
    作用—进程调度
    Pid=1:init进程
    作用—系统初始化

    编程调用getpid函数获取自身的进程标识符
    getppid获取父进程的进程标识符

什么叫父进程,什么叫子进程?

父进程(Parent Process):

  • 父进程是创建其他进程的进程。每个进程都是由另一个进程创建的,这个创建它的进程称为它的父进程。
  • 在Unix/Linux系统中,所有进程都是由系统的第一个进程(通常是 init 进程,PID为1)直接或间接创建的。

子进程(Child Process):

  • 子进程是由父进程创建的进程。子进程继承父进程的一些属性(如环境变量、打开的文件描述符等),但它有自己独立的内存空间和资源。

  • 子进程的创建通常通过系统调用(如 fork 或 spawn)完成。

关系:

  • 每个子进程都有一个唯一的父进程,父进程可以创建多个子进程
  • 父进程可以通过子进程的PID与其进行通信和管理。
  • 当父进程终止时,操作系统通常会自动终止其所有子进程,或者将子进程的父进程重新分配给 init 进程

进程的创建

fork函数创建

fork函数用于创建一个新的进程,这个新进程称为子进程。子进程是父进程的副本,但有一些不同之处:

  • 子进程有自己独立的地址空间。
  • 子进程从父进程继承文件描述符、信号处理方式等
  • fork函数调用成功,返回两次
    返回值为0, 代表当前进程是子进程
    返回值非负数,代表当前进程为父进程

代码示例

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

int main()
{
   pid_t pid;

   printf("father: id=%d\n",getpid());

   pid = fork();
   	
   if(pid > 0)
   {
   	printf("this is father print, pid = %d\n",getpid());
   }
   
   else if(pid == 0){
   	printf("this is child print,child pid = %d\n",getpid());
   }

   return 0;
}

在这里插入图片描述

vfork创建

vfork函数也是用于创建一个新的进程,但是它与fork有一些显著的区别:

  • 子进程和父进程共享相同的地址空间,不拷贝。
  • vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行

由于子进程和父进程共享地址空间,因此子进程在vfork之后不能改变父进程的内存内容,否则会引起不可预测的行为。vfork主要用于提高性能,因为它避免了复制父进程的整个地址空间

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

int main()
{
	pid_t pid;

	int cnt = 0;
	
	pid = vfork();
		
	if(pid > 0)
	{
			while(1){
			printf("cnt=%d\n",cnt);
			printf("this is father print, pid = %d\n",getpid());
			sleep(1);
		}	
	}	
	else if(pid == 0){	
		while(1){
			printf("this is chilid print, pid = %d\n",getpid());
			sleep(1);
			cnt++;
			if(cnt == 3){
				exit(0);
				break;
			}
		}	
	}
	return 0;
}

在这里插入图片描述

错误打印函数

#include <stdio.h>
void perror(const char *s);

功能:打印错误信息(某些函数返回负值,表明发生错误,但是不知道具体类型,使用这个函数可以获得具体错误类型)

进程的退出

正常退出

  • Main函数调用return。注意:return不是结束,只是函数结束,当它刚好结束的是main函数,此时导致进程结束。造成return结束进程的错觉。
  • 进程调用exit(),标准c库
  • 进程调用_exit()或者_Exit(),属于系统调用

异常退出

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

父进程等待子进程退出

为啥要等待子进程退出?

  • 资源释放:当一个子进程结束时,它的退出状态(exit
    status)和其他资源不会立即被系统回收,而是保存在系统中,直到父进程读取它们。这种状态的子进程被称为**“僵尸进程**”。如果父进程不处理这些退出状态,系统资源将被浪费,导致僵尸进程的积累。
  • 进程同步:父进程可能需要知道子进程的执行结果,以便根据结果执行进一步的操作。等待子进程退出可以确保父进程在子进程完成后进行相应的处理。
  • 进程树的清理:等待子进程退出有助于保持进程树的整洁。如果父进程没有等待子进程退出,系统中将会出现越来越多的僵尸进程,从而使进程树混乱。

什么是僵尸进程

在 Linux 系统中,当子进程终止时,它的进程控制块(Process Control Block,PCB)仍然保留在内存中。这是为了保存子进程的退出状态和其他信息,直到父进程能够读取这些信息。理解这一点的关键在于以下几个方面:

僵尸进程(Zombie Process):

  • 当一个子进程终止时,它的进程控制块不会立即被系统回收,而是转换为一个僵尸进程。

  • 僵尸进程保留了一些信息,如进程 ID、退出状态、资源使用统计等。

等待子进程(Wait for Child Process):

  • 父进程需要通过调用 wait() 或 waitpid() 等系统调用来读取子进程的退出状态。

  • 这些调用会返回子进程的退出状态,并从系统中删除僵尸进程,释放其占用的资源。

释放资源:

  • 如果父进程不读取子进程的退出状态,这些僵尸进程将一直存在,占用系统资源,最终可能导致资源耗尽。
  • 通过读取子进程的退出状态,父进程可以确保这些资源被正确释放

wait函数

wait函数用于让父进程等待其任意一个子进程结束。当子进程结束时,wait函数会返回子进程的PID,并通过参数status来返回子进程的退出状态。

函数原型

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

pid_t wait(int *status);

wait函数的返回值

  • 成功时返回子进程的PID。

  • 失败时返回-1,并设置errno来指示错误类型

  • status参数用于获取子进程的退出状态,可以使用宏来解析状态值

    WIFEXITED(status):如果子进程正常退出,则为真。
    WEXITSTATUS(status):如果WIFEXITED为真,则返回子进程的退出状态。
    WIFSIGNALED(status):如果子进程因信号而终止,则为真。
    WTERMSIG(status):如果WIFSIGNALED为真,则返回导致子进程终止的信号编号。

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

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


int main()
{
	pid_t pid;

	int cnt = 0;
	int status = 10;

	
	pid = fork();
		
	if(pid > 0)
	{

		wait(&status);
		printf("child quit, child status = %d\n",WEXITSTATUS(status));
		while(1){
			printf("cnt=%d\n",cnt);
			printf("this is father print, pid = %d\n",getpid());
			sleep(1);
		}	
	}
	
	else if(pid == 0){
		
		while(1){
			printf("this is chilid print, pid = %d\n",getpid());
			sleep(1);
			cnt++;
			if(cnt == 5){
				exit(3);
			}
		}	
	}

	return 0;
}

waitpid函数

waitpid函数提供了更灵活的方式来等待子进程的结束。可以指定等待特定的子进程,并且可以选择是否以非阻塞方式等待。

函数原型:

复制代码
#include <sys/types.h>
#include <sys/wait.h>

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

参数解释:

  • pid:指定等待的子进程的PID。

    -1:等待任意子进程(等同于wait函数)。
    正值:等待指定PID的子进程。
    0:等待与调用进程在同一进程组中的任何子进程。
    负值:等待与调用进程在同一进程组中的任何子进程,绝对值等于pid。

  • status:与wait函数中的status参数相同,用于返回子进程的退出状态。

  • options:可以是0或以下选项的组合:

    WNOHANG:如果没有已结束的子进程,则立即返回0。
    WUNTRACED:如果子进程已停止但未报告,则返回其状态。

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

int main()
{
	pid_t pid;

	int cnt = 0;
	int status = 10;

	pid = fork();
		
	if(pid > 0)
	{

//		wait(&status);
		waitpid(pid,&status,WNOHANG);
		printf("child quit, child status = %d\n",WEXITSTATUS(status));
		while(1){
			printf("cnt=%d\n",cnt);
			printf("this is father print, pid = %d\n",getpid());
			sleep(1);
		}	
	}
	
	else if(pid == 0){
		
		while(1){
			printf("this is chilid print, pid = %d\n",getpid());
			sleep(1);
			cnt++;
			if(cnt == 5){
				exit(3);
			}
		}	
	}

	return 0;
}

孤儿进程

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

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[]);

返回值:
  exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
参数说明:

  • path:可执行文件的路径名字

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

  • file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

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

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

具体可以在这里学习 点击跳转链接

system函数

在Linux系统中,system函数用于创建一个子进程,该子进程执行由字符串命令指定的shell命令。这是一个方便的方法来执行shell命令,并获取其退出状态。

函数原型

复制代码
#include <stdlib.h>

int system(const char *command);

参数

  • command:一个指向要执行的shell命令的字符串。如果command是NULL,system将检查是否存在默认的命令解释器(通常是/bin/sh),并返回一个非零值表示存在,否则返回0表示不存在。

返回值

  • 成功时,返回由shell返回的状态代码(即命令的退出状态)。如果shell无法执行,system返回127。
  • 失败时,返回-1,并设置errno以指示错误。

使用示例: 演示如何使用system函数来执行一个shell命令并获取其退出状态。

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

int main() {
    int status;

    // 执行一个简单的shell命令
    status = system("ls -l");

    if (status == -1) {
        perror("system");
        exit(EXIT_FAILURE);
    } else {
        // 解析命令的退出状态
        if (WIFEXITED(status)) {
            printf("Command exited with status %d\n", WEXITSTATUS(status));
        } else {
            printf("Command did not exit normally\n");
        }
    }

    return 0;
}

在这个示例中,system函数执行ls -l命令,并打印命令的退出状态。

system函数的工作原理

  1. system创建一个子进程。
  2. 子进程调用默认的shell(通常是/bin/sh)并传递命令字符串给shell。
  3. Shell解释并执行命令。
  4. 子进程等待命令执行完毕,并返回命令的退出状态给父进程。
  5. 父进程返回子进程的退出状态。

oppen函数

在Linux系统中,popen函数用于创建一个管道,连接到一个子进程的标准输入或标准输出。它可以让你通过C程序来执行一个命令,并且读取该命令的输出或者向该命令的输入写入数据。

函数原型

#include <stdio.h>

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

参数

  • command:一个指向要执行的shell命令的字符串。

  • type:表示管道方向的字符串。可以是"r"(读)或"w"(写)。

    “r”:打开一个管道,以读取子进程的标准输出。
    “w”:打开一个管道,以写入到子进程的标准输入。

返回值

  • popen:返回一个指向FILE的指针,用于I/O操作。如果失败,返回NULL。
  • pclose:返回命令的终止状态。如果调用失败,返回-1。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
int main(void)
{
    char ret[1024] = {0};
    FILE *fp;

    fp = popen("ps","r");
    int nread = fread(ret,1,1024,fp);	

    printf("read ret %d byte, ret=%s\n",nread,ret);
	    
    return 0;
}

在这里插入图片描述

区别system函数
比system在应用中的好处:可以获取运行的输出结果

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值