Linux编程之进程

Linux进程

什么是程序,什么是进程?

简单的来说,没有跑起来的文件叫做程序(是静态概念),例如调用gcc test.c -o test 生成的这个test文件,它就是一个程序,当这个程序跑起来就是进程(动态概念)。进程是程序的一次执行,也就是说每执行一次程序,它就会生成一个新的进程。

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

ps -aux
//查看当前运行的进程

由于当前的程序时非常多的,所以我们要使用grep指令进行筛选,例如:查看当前进程中带有 “init” 字样的进程

ps -aux|grep init

在这里插入图片描述

什么是进程标识符?

每个进程都有一个唯一非负整数表示它的进程标识符(类似于之前的文件描述符),用pid表示,它是操作系统用来区分和管理各个进程的一种方法。

Linux中有pid = 0,pid = 1,pid = 2三个特殊的进程。

  • pid = 0,称为调度进程(交换进程),它是唯一一个没有通过fork或者kernel_thread()函数产生的进程,也是其他进程的父进程。它的主要作用是在没有进程可以被调度时运行,不做具体的事情。例如当进程中使用fork函数创建了子进程,那么当两个进程运行的时候,它就受调度进程的控制
  • pid = 1,系统初始化进程(init),是pid = 0 的子进程,是所有其他用户进程的父进程。当内核完成初次启动后,它会自动创建并运行init进程。简单来说:每个不同牌子的电脑在开机时都会运行它的开机动画,这个过程就可以认为是init进程去调用了其他的进程来显示开机页面。init进程的另外一个作用是防止系统产生孤儿进程。
  • pid = 2,pid = 2的进程是"kthreadd"进程,由系统自动创建的第一个内核进程,也是所有内核空间进程的父进程。它主要负责管理内核线程的创建和销毁。

获取进程标识符:getpid() / getppid()函数

函数原型:

pid_t getpid(void);	//此函数返回一个pid_t类型的进程标识符
pid_t getppid(void);	//用于获取父进程的进程ID

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

简单来说假如进程A创建了进程B,那么A就是B的父进程,B就是A的子进程。

获取进程的ID:

#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"

int main()
{
	pid_t pid;

	pid = getpid();	//获取当前进程的ID
	printf("pid = %d\n",pid);

	return 0;
}

创建进程:fork()

函数原型:

pid_t fork(void);

fork函数在执行时会返回两个值。这两个返回值分别代表父进程和子进程的返回值。

  1. 在父进程中,fork函数的返回值是新创建的子进程的进程ID。如果fork函数执行失败,那么它会返回一个负值。
  2. 在子进程中,fork函数的返回值则是0。

调用fork() 系统发生的事情:系统首先会为新的进程分配资源,包括存储数据和代码的空间。然后,系统会将原进程的所有值复制到新的进程中,但两者的某些属性值可能会不同。

具体来说,在父进程中,fork函数返回新创建的子进程的进程ID;而在子进程中,fork函数则返回0。这种设计允许程序通过检查fork函数的返回值来判断当前进程是父进程还是子进程。值得注意的是,虽然新创建的子进程几乎与父进程完全相同,但两者仍然是两个独立的进程,它们各自拥有自己的地址空间,形式上有点像把一个文件拷贝了一个副本。因此,虽然父子进程之间不会共享资源,也不会互相影响,但它们可以并行执行,从而提高系统的并发性能。

代码说明1:
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"

int main()
{
	pid_t pid;
	pid_t pid2;

	pid = getpid();
	printf("father's process pid is %d\n",pid);	//在未调用fork之前只有一个进程,此时打印的是父进程的pid

	fork();		//调用fork函数会生成一个新的子进程,此时有两个进程在执行,以下的代码两个进程都会执行

	pid2 = getpid();	//此时获得fork函数以后的进程标识符,两个进程对应两个进程标识符
	printf("after fork pid is %d\n",pid2);

	if(pid == pid2)		//如果此时的pid2和之前的pid相等,那么它就是父进程
	{
		printf("this is father's pid is %d\n",getpid());	//此时的父进程和之前pid一定是相等的
	}
	else	//如果fork以后的pid2和之前的pid不相等,那么它就是通过fork函数创建的新的子进程
	{
		printf("this is child's pid is %d father's pid is %d\n",getpid(),getppid());
	//此时调用getpid函数获取进程标识符,在子进程中使用getppid获取父进程的pid
	}

	return 0;
}

编译结果

image-20240122145855234

代码说明2:
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"

int main()
{
	pid_t retpid;

	retpid = fork();	//调用fork函数会生成一个新的子进程,此时有两个进程在执行,以下的代码两个进程都会执行
	
	if(retpid > 0)	//如果fork的返回值大于0,那么它就是父进程,返回的值就是子进程的ID
	{
		printf("this is father's process pid is %d retpid = %d\n",getpid(),retpid);
	}
	else if(retpid == 0)	//如果fork的返回值等于0,那么它就是子进程
	{
		printf("this is child's process pid is %d,retpid = %d\n",getpid(),retpid);
	}
	else	//如果fork的返回值是负数,那么fork出错,创建子进程失败
	{
		printf("fork error\n");
	}


	return 0;
}

父进程通过调用fork函数创建子进程,如果父进程和子进程里边有调用while(1)循环,而且父进程没有调用 wait() 或者 waitpid() 函数来等待子进程先执行,那么父进程和子进程就会同时执行,两个进程会争夺CPU资源,由进程调度来分配哪个先执行。

#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"

int main()
{
	pid_t retpid;

	retpid = fork();	

	if(retpid > 0)	
	{
		while(1)		//父进程没有调用wait或者waitpid函数,两个进程一起执行
		{
			printf("this is father's process pid is %d retpid = %d\n",getpid(),retpid);
			sleep(1);
		}
	}
	else if(retpid == 0)	
	{
		while(1)
		{	
			printf("this is child's process pid is %d,retpid = %d\n",getpid(),retpid);
			sleep(1);
		}
	}
	else	
	{
		printf("fork error\n");
	}

	//当没有使用wait或者waitpid函数的时候,两个进程会争夺CPU资源,执行顺序由进程调度分配
	return 0;
}

编译结果

在这里插入图片描述

创建进程:vfork()函数

函数原型:

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

pid_t vfork(void);

vfork函数和fork函数一样是用来创建进程的,但是它和fork函数有所不同:

  1. 当使用fork函数的时候,新建的子进程和父进程的运行次序是不定的;如果使用vfork函数创建子进程,父进程在子进程运行时是处于阻塞状态的,直到子进程调用exit退出或者exec函数后,父进程才会被唤醒继续执行。这意味着,在子进程运行的期间,父进程是无法被调度运行的。
  2. 另一个重要的区别是,vfork函数在创建子进程的时候,不会复制父进程的地址空间,而是会和父进程共享一片地址空间。这就意味着,子进程可以直接访问父进程的地址空间。如果在子进程在未退出时修改了某一变量的值,那么它就会映射到父进程中,同样去改变父进程中的值。
代码说明:
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"

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

	retpid = vfork();	

	if(retpid > 0)	
	{
		while(1)
		{
			printf("this is father's process pid is %d retpid = %d\n",getpid(),retpid);
			printf("data = %d\n",data);
			sleep(1);
		}
	}
	else if(retpid == 0)	
	{
		while(1)
		{	
			printf("this is child's process pid is %d,retpid = %d\n",getpid(),retpid);
			sleep(1);
			data = data + 100;
			printf("data = %d\n",data);
			cnt++;
			if(cnt == 6)
			{
				exit(0);
			}
		}
	}
	else	
	{
		printf("fork error\n");
	}
	
	return 0;
}

编译结果

在这里插入图片描述

这里我在子进程中修改了data的值,在子进程运行结束后,父进程开始运行。发现父进程中data的值也被改变了。因此,对于需要避免数据被修改或多个进程同时访问同一数据的场景,vfork函数可能不是最合适的选择。

进程退出

正常退出:

  1. 在main函数中使用关键字return。当程序执行到main函数的结尾处,遇到return语句,系统会调用 exit() 函数来终止进程。
  2. 手动调用exit()或exit()函数。这两个函数都可以立即终止当前进程。其中,exit()函数会将控制权交给系统,而exit()函数则直接将控制权交回给内核。

exit()函数

函数原型:

#include <stdlib.h>

void exit(int status);
//函数的参数是一个整数,表示进程退出的状态码。状态码通常用于指示程序执行过程中是否发生了错误或异常情况。通常情况下,状态码为0表示程序正常退出,非零值表示程序异常退出。

这里需要注意在程序退出的时候,尽量不要调用break,因为调用break可能导致某些数据丢失。

异常退出:

  1. 调用abort

  2. 当进程收到某些信号时,如ctrl+C

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

因为我们创建子进程的目的是要让它去办除父进程以外的另一件事,所有我们就要关心它事情办得怎么样。我们可以等待子进程退出,然后将状态码返回给父进程,父进程去判断子进程此时的状态。

wait()函数

wait函数是一个系统调用函数,主要用于父进程等待子进程退出,并收集子进程的状态

函数原型:

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

 pid_t wait(int *wstatus);
//它只有一个参数,即指向变量的指针,这个参数用于存储子进程的退出状态信息
//wait的参数如果是空的话,那么就不关心子进程的退出状态;如果是一个整形指针,那么子进程退出的状态码就存放在它所指向的地址中。

在Linux中,wait有三个功能:

  1. 组赛等待子进程的退出;
  2. 回收子进程的残留资源;
  3. 获取子进程的结束状态。

子进程通过调用exit将状态码传给父进程,然后父进程通过调用wait函数收集子进程的状态码,并将其存放到其参数status里。然后通过以下的宏来解析状态码,用户可以将其打印出来查看子进程此时的状态,是否正常退出等等。

在这里插入图片描述

代码演示
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
#include "sys/wait.h"

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

	pid = fork();
	if(pid > 0)
	{
		wait(&status);	//父进程调用wait函数将自己挂起等待子进程退出,并且收集子进程退出的状态码存放到status中
		printf("the child's exit :status = %d\n",WEXITSTATUS(status));//然后用这个宏将状态码解析并打印出来

		while(1)
		{
			printf("this is father's process,pid is %d\n",getpid());
			sleep(1);
		}
	}
	else if(pid == 0)
	{
		while(1)
		{
			printf("this is child's process,pid is %d\n",getpid());
			sleep(1);
			cnt++;
			if(cnt == 5)	//当子进程执行完cnt==5的时候调用exit函数,然后将退出码传给父进程
			{
				exit(0);
			}
		}
	}
	else
	{
		printf("fork error\n");
	}
	return 0;
}

编译结果
在这里插入图片描述

僵尸进程

子进程在退出的时候会向父进程发送SIGCHLD信号,通知其子进程已经终止。如果父进程没有收到此信号或者忽略了此信号并未对其进行相应的处理,那么子进程就会一直处于僵尸状态,无法正常退出,直到父进程来回收子进程。尽管它已经不再运行任何程序,但是仍然占用着系统的一些资源(如进程号、文件描述符等)。父进程没有对子进程退出状态进行处理,此时子进程就会变成一个僵尸进程。

关于僵尸进程是否会一直占用系统资源的问题,答案是不会的。当父进程结束时,它会释放子进程的资源,这时僵尸进程就会被系统清除,其所占用的资源也会被完全释放。

Linux操作系统有多种运行状态,下面是一些常见的运行状态:

  1. 运行(Running):表示进程正在运行,占用CPU资源,并且可以被调度执行。

  2. 等待(Waiting):表示进程正在等待某个事件的发生,例如等待I/O操作完成、等待信号量、等待锁等。在等待状态下,进程不会占用CPU资源,直到等待的事件发生。

  3. 停止(Stopped):表示进程被暂停执行,不再占用CPU资源。进程可以被其他进程发送的信号唤醒,继续执行。

  4. 僵尸(Zombie):表示进程已经终止,但是其父进程还没有调用wait()waitpid()来获取其终止状态。僵尸进程占用系统资源很少,但是如果存在大量的僵尸进程,可能会导致系统资源耗尽。

  5. 停滞(Idle):表示系统处于空闲状态,没有正在运行的进程。这种状态下,系统通常会执行一些空闲任务,例如定期更新系统状态、清理缓存等。

  6. 睡眠(Sleeping):表示进程暂时不可运行,等待某个事件的发生。与等待状态类似,但是睡眠状态下的进程可以被其他进程唤醒。

  7. 僵尸停止(Zombie Stopped):表示停止状态的僵尸进程,即进程已经终止,但是其父进程还没有获取其终止状态。

这些是Linux操作系统中常见的进程运行状态。进程的状态会随着其执行过程中的事件变化,通过命令如pstop等可以查看进程的当前状态。

waitpid()函数

waitpid函数是一个在Linux系统中用于父进程等待子进程退出的系统调用。

函数原型:

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

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

它有三种形式
waitpid(-1, &status, 0):等待任何子进程结束,并返回第一个结束的子进程的状态信息。
waitpid(pid, &status, 0):等待指定的子进程pid结束,并返回该子进程的状态信息。
waitpid(pid, &status, options):等待指定的子进程pid结束,并返回该子进程的状态信息。options参数可以指定额外的选项,如是否忽略SIGCHLD信号等。

waitpid函数的第一个参数pid用于指定要等待的子进程的PID。它可以取以下几个值:

  • 正整数:等待具有指定PID的子进程。
  • 0:等待与调用进程属于同一个进程组的任何子进程。
  • -1:等待任何子进程。
  • 负整数:等待进程组ID等于pid绝对值的任何子进程。

status是一个指向整数数组的指针,用于存储子进程的状态信息。状态信息包括:进程ID、退出状态、终止信号等信息。如果成功,waitpid函数返回结束的子进程的进程ID;如果发生错误,则返回-1。

第三个参数options有以下的选项:

  • WNOHANG:此选项使waitpid函数在没有子进程退出时立即返回,而不会像wait那样永远等待。如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回。这样可以避免父进程无限期地阻塞。
  • WUNTRACED:此选项表示如果子进程进入暂停状态(如被暂停或停止),则waitpid也会返回。也就是说,即使子进程处于暂停状态,waitpid也会返回,并告知其父进程子进程的终止状态。
代码示例
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
#include "sys/wait.h"

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

	pid = fork();
	if(pid > 0)
	{
		waitpid(pid,&status,0);	//这里调用waitpid()函数也会对子进程的退出状态进行收集,所以子进程不会变成僵尸进程
		printf("the child's exit :status = %d\n",WEXITSTATUS(status));//然后用这个宏将状态码解析并打印出来

		while(1)
		{
			printf("this is father's process,pid is %d\n",getpid());
			sleep(1);
		}
	}
	else if(pid == 0)
	{
		while(1)
		{
			printf("this is child's process,pid is %d\n",getpid());
			sleep(1);
			cnt++;
			if(cnt == 5)	//当子进程执行完cnt==5的时候调用exit函数,然后将退出码传给父进程
			{
				exit(0);
			}
		}
	}
	else
	{
		printf("fork error\n");
	}
	return 0;
}
//并且这里调用waitpid()函数也会对子进程的退出状态进行收集,所以子进程不会变成僵尸进程

100219

值得注意的是:这里如果采用第三种形式,然后把options的选项设置为WNOHANG模式,此时父进程不再处于阻塞状态,会和子进程一起运行。以上边的代码为例,如果将 waitpid() 函数写在while(1)的外边,那么当程序开始执行,父进程只执行一次 waitpid() 函数,而子进程还没有退出,所以它就捕获不到子进程的退出码,那么子进程还是会变成僵尸进程。可以将 waitpid() 函数写到 while(1) 里边不断的检测子进程是否退出来获取子进程的退出码。

waitpid()只执行一次:子进程还是会变成僵尸进程

孤儿进程

孤儿进程是指一个父进程在创建了一个子进程后,父进程在子进程结束之前先先一步结束,而子进程还在运行。此时,这个子进程就被称为孤儿进程。Linux系统不允许存在过多的孤儿进程,所以孤儿进程会被init进程(PID为1)接管,并由它负责回收孤儿进程的资源和结束孤儿进程的运行。(但是要注意的是在实际运作的过程中孤儿进程不一定会被init进程收留,可能会被别的进程收留)

代码演示
#include "sys/types.h"
#include "stdio.h"
#include "unistd.h"
#include "stdlib.h"
#include "sys/wait.h"

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

	pid = fork();
	if(pid > 0)
	{
		printf("this is father's process,pid is %d\n",getpid());
	}
	else if(pid == 0)
	{
		while(1)
		{
			printf("this is child's process,pid is %d,father's process pid is %d\n",getpid(),getppid());
			sleep(1);
			cnt++;
			if(cnt == 5)	//当子进程执行完cnt==5的时候调用exit函数,然后将退出码传给父进程
			{
				exit(3);
			}
		}
	}
	else
	{
		printf("fork error\n");
	}
	return 0;
}

image-20240121215440602

代码运行的过程中父进程打印了它的pid以后就结束了,此时父进程还是刚刚标号为19166的进程,但是父进程已经结束,它就变成了一个孤儿进程,但是系统又不允许它存在过多的孤儿进程,所以它会被init进程收留(或者时别的进程,这里就是被999进程收留),然后收留的进程就会变成它的父进程,最终被收留的进程结束掉。

exec族函数

exec函数族是一组在Linux系统中用于在调用进程内部执行一个可执行文件的函数。这些可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。exec函数族的主要作用是根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。

exec函数族的成员包括:execl, execlp, execle, execv, execvp, execvpe。这些函数在功能上非常相似,主要的区别在于参数的传递方式。

exec族函数的后缀

  • l(list): 表示参数以逗号分隔的形式列表给出,并以 NULL 结尾。
  • v(vector): 表示参数通过一个指针数组传递,该数组包含了指向每个参数的指针,并以 NULL 结尾。
  • p(path): 表示在 PATH 环境变量指定的目录中搜索可执行文件。
  • e(environment): 表示提供一份环境变量列表,这份列表是一个包含指向每个环境变量字符串的指针数组。

声明

#include <unistd.h>

extern char **environ;


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

参数

  • path参数是一个字符串,表示要执行的可执行文件的路径。
  • arg0参数是一个字符串,表示新程序的名称。
  • envp参数是一个字符串数组,每个字符串表示一个环境变量,以NULL作为结束标志。
  • file参数是一个字符串,表示要执行的可执行文件的名称。
  • argv参数是一个字符串数组,每个字符串表示一个命令行参数,以NULL作为结束标志。

返回值

这些函数在执行成功时不会返回,只有在出错时才会返回-1,并设置errno来指示错误的原因。它们可以用于在当前进程中执行新程序,替换当前进程的映像,并且可以传递命令行参数和环境变量给新程序。

示例:execl
/*******execl.c********/
#include "stdio.h"
#include "unistd.h"

int main()
{
    printf("before execl\n");
	//int execl(const char *path, const char *arg1, ... /* 可变参数 */);函数原型
    if(execl("./test","test",NULL) == -1)
    {
        printf("execl error");
       	perror("reason: ");		//execl函数执行失败会设置errno,可以用perror来将失败的原因打印出来
    }
    printf("this one may not be printed\n");	//如果execl函数成功执行,这句话不会被打印,否则打印
    return 0;
}
/******test.c******/
#include "stdio.h"

int main()
{
    int arr[3] = {10,20,30};
    printf("this is execl invoke\n");
    for(int i=0;i<sizeof(arr)/sizeof(arr[0]);i++)
    {
        printf("%d ",arr[i]);
    }
    putchar('\n');

    return 0;
}

编译结果

image-20240122120236826

通过编译结果可知在execl这个程序执行的过程中,通过调用execl函数执行了test.c的程序,而且test.c程序执行完后并不会返回原程序继续执行。

示例:execlp
//execlp.c
#include "stdio.h"
#include "unistd.h"

int main(int argc,char **argv)
{
	printf("before execlp\n");
	if(execlp("date","date",NULL,NULL) == -1)
	{
		printf("execlp error");
		perror("execlp");
	}
	printf("this line will not be printed\n");

	return 0;
}

编译结果

image-20240122120636752

通过程序可以发现此时使用date指令并没有加路径,但是编译执行成功,所以execlpexecl函数区别就在于是否需要加路径,execlp函数可以通过绝对路径自己去找当前指令所在的路径,不需要手动添加。

打印绝对路径:

echo $PATH

打印当前路径:

pwd
示例:execv
//execv.c
#include "stdio.h"
#include "unistd.h"

int main(int argc,char **argv)
{
	printf("before execv\n");
	char *arg[] = {"date",NULL,NULL};
	if(execv("/usr/bin/date",arg) == -1)
	{
		printf("execv error");
		perror("execv");
	}
	printf("this line will not be printed\n");

	return 0;
}

编译结果

image-20240122121211442

通过代码可以发现execv就是将后边的参数构建成一个数组,然后结合路径使用

示例:execvp
/******ls指令******/

#include "stdio.h"
#include "unistd.h"

int main()
{
    printf("before execl\n");
    char *argv[] = {"ls","-l",NULL};
    if(execvp("ls",argv) == -1)	
    {
        printf("execvp error\n");
        perror("reason: ");
    }
    printf("this one may not be printed\n");
    
    return 0;
}

编译结果

image-20240122121738158

execvpexecv的区别就是是否需要添加路径,它们的区别就像execlexeclp的区别,execvp函数也同样会通过绝对路径来寻找可执行文件的路径

exec族函数的应用

  • 进程替换:当一个进程完成了它的某部分任务后,如果想要继续执行另一个程序,而不是终止当前进程,可以使用 exec 函数族中的某个函数来替换当前进程的映像,从而实现进程的“转生”。
  • 任务分配:在多任务环境中,父进程可以通过 fork 创建一个子进程,然后通过子进程调用 exec 函数族中的函数来执行特定的任务。这样可以让父进程继续执行其他任务,而子进程则专注于新程序的执行。
  • 程序启动:在某些情况下,程序可能需要在运行时动态地加载和执行其他程序或脚本。例如,shell 命令解释器会使用 exec 函数族来执行用户输入的命令。

此外,exec 函数族还可以用来改变进程的环境变量或者执行路径,这可以通过传递不同的参数来实现。例如,execleexecvpe 允许你指定一个环境变量数组,而 execlpexecvp 则会在 PATH 中搜索可执行文件。

总的来说,exec 函数族提供了一种在不创建新进程的情况下执行新程序的能力,这使得它们在进程管理和任务分配中非常有用。通过选择合适的 exec 变体,可以根据需要灵活地传递参数和环境设置。

示例:exec&fork

父进程通过fork函数创建子进程,子进程去修改文本的内容

如果不使用exec族函数,那么就需要在子进程里去编写有关修改文本文件的代码,如下图:

image-20240122125556096

但是如果现在已经有了现成的修改文本文件的代码,那么只需要在子进程中去调用这个可执行文件,缩小了代码的篇幅。

#include "stdio.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "unistd.h"
#include "string.h"
#include "stdlib.h"
#include "sys/wait.h"

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

	while(1)
	{
		printf("Please enter the num:\n");
		scanf("%d",&num);

		if(num == 1)
		{
			pid = fork();

			if(pid > 0)
			{
				printf("this is father's process\n");
				wait(NULL);
			}
			if(pid == 0)
			{
				execl("./changedata","changdata","config.txt",NULL);	//直接在子进程中调用execl函数,去调用可执行文件changedata
			}
		}
		else
		{
			printf("wait,do nothing\n");
		}
	}

	return 0;
}
//config.txt

#include "stdio.h"

int main()

	return 0;
}
//changedata.c

#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "string.h"
#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"

int main()
{
	int fpSrc;
	char *readBuf = NULL;

	fpSrc = open("./config.txt",O_RDWR);
	if(fpSrc > 0)
	{
		printf("open src file successfully\n");
	}

	int size = lseek(fpSrc,0,SEEK_END);
	lseek(fpSrc,0,SEEK_SET);
	printf("the size of file :%d\n",size);

	readBuf = (char *)malloc(sizeof(size));
	int n_RD = read(fpSrc,readBuf,size);
	printf("read %d byte from the src file\n",n_RD);

	char *p = strstr(readBuf,"mian");
	*(p+1) = 'a';
	*(p+2) = 'i';

	lseek(fpSrc,0,SEEK_SET);
	int n_write = write(fpSrc,readBuf,size);
	printf("write %d byte to the file\n",n_write);

	close(fpSrc);

	return 0;
}

编译结果

image-20240122124610811

image-20240122124632700

image-20240122124644693

system函数

system函数在Linux下主要用于执行shell命令。

首先,system函数通过创建子进程来执行传入的命令字符串,这个过程涉及到forkexecl系统调用。在命令执行期间,当前进程会被阻塞,直到子进程完成命令执行。此外,system函数会忽略或阻塞某些信号,如SIGCHLD,以确保子进程的正确回收和状态返回。

其次,system函数的原型是int system(const char *command);,其中command是要执行的shell命令。函数的返回值通常是命令的退出状态码,如果无法启动shell或出现其他错误,则返回-1或127。

最后,在实际使用中,需要注意system函数的安全性问题,因为它可能会受到注入攻击,特别是当命令字符串来源于不可信的用户输入时。为了提高安全性,可以结合信号处理来确保子进程的正确管理,例如在调用system前后恢复SIGCHLD信号的处理函数。

简单来说,system函数就是封装好的exec族函数

示例:system
#include "stdio.h"
#include "stdlib.h"

int main()
{
	printf("before system\n");
	if(system("./test") == -1)	
	{
		printf("system error");
	}
	printf("after system\n");

	return 0;
}
/******test.c******/
#include "stdio.h"

int main()
{
    int arr[3] = {10,20,30};
    printf("this is execl invoke\n");
    for(int i=0;i<sizeof(arr)/sizeof(arr[0]);i++)
    {
        printf("%d ",arr[i]);
    }
    putchar('\n');

    return 0;
}

编译结果

在这里插入图片描述

值得注意的是system函数在执行完调用函数后还会返回原函数,这个和exec族函数不一样。

popen函数

调用system函数后,指令的执行结果会在shell上打印出来,如果想要把执行的结果保存到一个数组里,system函数没有这个功能,所以引入popen函数。

声明

#include <stdio.h>

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

int pclose(FILE *stream);	//关闭文件流

参数

  • command 是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 /bin/sh 并使用 -c 标志,shell 将执行这个命令。
  • type 指定了管道的类型,它可以是 “r”(读取模式)或 “w”(写入模式)。如果 type 是 “r”,则文件指针连接到 command 的标准输出;如果 type 是 “w”,则文件指针连接到 command 的标准输入。

返回值

  • 如果调用成功,popen 返回一个 FILE 指针,该指针可以用于读取或写入数据,取决于 type 参数的值。如果调用失败,popen 返回 NULL,具体错误可以通过检查 errno 来确定。

总的来说,popen 函数提供了一个比 system 更加灵活的方式来与子进程进行交互,因为它允许你直接读取或写入子进程的输入和输出。这使得 popen 在需要处理命令输出或向命令提供输入的情况下非常有用。

popen函数和open函数一样都会返回一个文件流,而且最后都需要把这个文件流关闭,防止文件损坏。

示例:popen

将指令的执行结果保存到数组里,并且打印出来

#include "stdio.h"

int main()
{
    FILE *fp;
    char readBuf[1024];
    
    fp = popen("ps","r");
    if(fp == NULL)
    {
        printf("popen failed\n");
    }
    while(fgets(readBuf,sizeof(readBuf),fp) != NULL)	//fgets会一直读取,直到读取到文件末尾会停止读取
    {
        printf("%s",readBuf);
    }
    pclose(fp);	//关闭管道
    
    return 0;
}

编译结果

image-20240122142633589

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

日落星野

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值