• Linux 进程操作


    进程的基本知识

    一个程序的执行称为一个进程,所有的代码都是在进程中执行的,进程是操作系统资源分配的基本单位。

    Linux中,除了内核的启动进程外,其他所有的进程都是由它的父进程通过fork函数调用产生的。

    操作系统通过识别每一个进程的pid来识别每一个进程,pid在进程中是唯一的,可以重复利用(比如说前一个为pid=11的进程死掉了,那么pid=11的这个进程就可以分配给其他进程使用了:当pid达到最大限制时,内核会从头开始查找闲置的pid并使用最先找到的那一个作为新进程的id)
    在这里插入图片描述

    进程pid

    操作系统中有一些进程的id是专用的:

    • id=0的进程为调度进程,调度进程是内核的一部分,并不执行任何磁盘上的程序
    • id=1的进程叫做 init 进程,init 进程是以超级用户权限运行着的普通用户进程,不是系统进程。

    进程常用的函数

    • 头文件:#include
    • __pid_t getpid (void):调用进程的进程ID
    • __pid_t getppid (void):调用进程的父进程ID
    • __uid_t getuid (void) :调用进程的实际用户ID
    • __uid_t geteuid (void):调用进程的有效用户ID
    • __gid_t getegid (void):调用进程的有效组ID

    fork

    • fork用于进程的创建(完整的复制),使用fork()函数要引入头文件#include,其函数原型为pid_t fork(void),fork返回值是pid_t,本质上是int类型。
    • 对于fork的返回值,如果是成功,子进程的id号会返回给父进程,并且把0返回给子进程;如果失败了,-1会返回给父进程,并且不会有子进程的创建,
    //举一个简单的例子
    #include
    #include
    #include
    int main(){
    	pid_t n=fork();
    	if(n==0){//说明这是一个子进程
    		printf("child:my pid=%d.,my ppid =%d\n",getpid(),getppid());
    	}else if(n==-1){
    		printf("程序创建错误\n");
    		return -1;
    	}else{
    		sleep(1)//保证子进程可以执行完成
    		printf("parent:my pid =%d,n=%d\n",getpid(),n);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    [cch@aubin s]$ gcc file.c
    [cch@aubin s]$ ./a.out
    child:my pid=3585.,my ppid =3584
    parent:my pid =3584,n=3585
    [cch@aubin s]$ 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    以上的程序结果显示的是子进程先执行,而后父进程开始执行,但是实际上,在执行了fork之后,父进程与子进程都是同时存在的,也就是child:my pid=3585.,my ppid =3584语句和parent:my pid =3584,n=3585语句是同时执行的(并发执行),只是在执行父进程的时候进行了睡眠而已。

    可能会出现以下的执行情况:

    parent:my pid =3584,n=3585
    [cch@aubin s]$ child:my pid=3585.,my ppid =3584
    
    • 1
    • 2

    这是因为在并发执行的过程中,父进程先执行完毕,父进程执行完毕会返回给父进程的父进程,也就是系统调用,然后显示出来美元符号$,紧接着子进程也执行完毕,想要返回父进程会发现父进程已经结束了,只能返回上一层的上一层,也就是在美元符号后面显示。

    wait和waitpid

    • 库函数exit(status)终止一进程,将进程占用的所有资源(内存、文件描述符等)归还内核。参数status为一整形变量,标识进程的退出状态。父进程可以通过系统调用wait()来获取该状态
    • 系统调用wait(&status)的目的有二:
      其一:如果子进程尚未调用exit()终止,那么wait()会挂起父进程直到有一个子进程终止
      其二:子进程的终止状态通过 wait()的 status 参数返回
    #include
    #include
    #include
    #include
    #include
    
    int main(){
    	pid_t pid;
    	char *message;
    	int n;
    	int exit_code;
    
    	pid=fork();
    	switch(pid){
    		case -1:exit( -1);
    		case 0:
    			message="child child";
    			n=5;
    			exit_code=37;
    			break;
    		default:
    			message="father father";
    			n=3;
    			exit_code=38;
    			break;
    	}
    	for(;n>0;n--){
    		puts(message);
    		sleep(1);
    	}
    
    	if(pid){//父进程
    		int stat_val;
    		pid_t child_pid;
    		child_pid=wait(&stat_val);//父进程等待子进程的结束
    
    		printf("child finish:pid=%d\n",child_pid);
    		if(WIFEXITED(stat_val))//子进程正常终止的情况
    			printf("child exit with code %d\n",WEXITSTATUS(stat_val));
    		else//子进程非正常终止的情况
    			printf("child terminated abnormally\n");
    	}
    	exit(exit_code);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    [cch@aubin s]$ gcc file.c 
    [cch@aubin s]$ ./a.out
    father father
    child child
    father father
    child child
    father father
    child child
    child child
    child child
    child finish:pid=4459
    child exit with code 37
    [cch@aubin s]$ 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • pid_t wait(int *status)用于等待子进程结束,回收并且释放子进程资源;其中status用来保存子进程退出时的状态,如果status为NULL,表示忽略子进程退出时的状态;如果函数执行成功,返回子进程的进程号,失败则返回-1。
    • pid_t waitpid(pid_t pid ,int *status , int options)waitpid相比于wait多了两个参数,options=0时,同wait,阻塞父进程,等待子进程退出。

    op

    exec函数簇

    • 用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。
    • 功能:在调用进程内部执行一个可执行文件,可执行文件既可以是二进制文件,也可以是任何linux下可执行的脚本文件。但是一定要是可以直接执行的文件,那不是那些有编译链接的文件
    • exec本身不是一个函数,而是有6种不同的函数可供使用,所以被称为exec函数簇
    #include 
     
    int execl( const char *pathname, const char *arg0, ... /* (char *)0 */ );
     
    int execv( const char *pathname, char *const argv[] );
     
    int execle( const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ );
     
    int execve( const char *pathname, char *const argv[], char *const envp[] );//进程映像的名字,映像所携带的参数,参数为0表示结束
     
    int execlp( const char *filename, const char *arg0, ... /* (char *)0 */ );
     
    int execvp( const char *filename, char *const argv[] );
     
    6个函数返回值:若出错则返回-1,若成功则不返回值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    exec函数族一般规律

    exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。

    • l (list) 命令行参数列表
    • p (path) 搜素file时使用PATH变量
    • v (vector) 使用命令行参数数组
    • e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量事实上。

    只有execve是真正的系统调用,其它五个函数最终都调用execve

    #include
    #include
    #include
    #include
    #include
    
    int main(){
    	printf("exec:\n");
    	execlp("ls","ls,0");
    	printf("dome\n");
    	exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    [cch@aubin s]$ gcc file.c 
    [cch@aubin s]$ ./a.out
    exec:
    dome
    [cch@aubin s]$ 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    详细参考:https://blog.csdn.net/u014530704/article/details/73848573

    system函数

    • int system(const char * command)system函数包含在头文件stdlib.h中,该函数会执行dos(windows系统) 或 shell(Linux/Unix系统) 命令,参数字符以command为命令名字。
    • 在windows系统中,system函数直接在控制台调用一个command命令。
      在Linux/Unix系统中,system函数会调用fork函数产生子进程,由子进程来执行command命令,并且使用wait阻塞父进程,直到子进程执行完毕后父进程继续执行
    • 命令执行成功返回0,执行失败返回-1。
    #include
    #include
    
    int main(){
    	printf("system:\n");
    	system("ls");
    	printf("dome\n");
    	exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    [cch@aubin s]$ gcc file.c 
    [cch@aubin s]$ ./a.out
    system:
    a.out  file.c
    dome
    [cch@aubin s]$ 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    system("ls &");//表示把子进程放到后台去做
    
    • 1

    信号处理signal函数

    Linux中的信号可以通过kill -l命令获取

    #include
    #include
    #include
    
    void ouch(int sig){
    	printf("signal:%d\n",sig);
    	(void)signal(SIGINT,SIG_DFL);//将ctrl+c的对应信号处理函数恢复成默认的值,也就是中断当前程序的执行
    }
    
    int main(){
    	(void)signal(SIGINT,ouch);//捕获信号ctrl+c,ouch是信号处理函数
    	while(1){
    		printf("hello world\n");
    		sleep(1);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • void(* signal(int sig,void(* func)(int)))(int);指定使用sig指定的信号编号处理信号的方法。 其中void(* func)(int)表示的是一个带有int类型参数的函数指针;而signal本身的函数的返回值也是一个函数指针
    • 常用的sig包括(共32):
      • SIGINT:2,表示interrupt中断
      • SIGKILL:9,表示发送不可屏蔽的kill信号
      • SIGSEGV:11,表示segmentation violation,段违规
      • SIGUSR1:10,开放用户使用,默认处理进程终止
      • SIGUSR2:12,开放用户使用,默认处理进程终止
    • 参数func指定程序可以处理信号的三种方式之一:
      • 默认处理(SIG_DFL):信号由该特定信号的默认动作处理。
      • 忽略信号(SIG_IGN):忽略信号,即使没有意义,代码执行仍将继续。
      • 函数处理程序:定义一个特定的函数来处理信号。

    除了在终端,也可以使用kil -2 pid来给对应的进程发送信号,表示系统向程序发送信号

    Linux的SIGUSR1SIGUSR2

    • 当一个进程调用fork时,因为子进程在开始时复制父进程的存储映像,信号捕捉函数的地址在子进程中是有意义的,所以子进程继承父进程的信号处理方式。
    • 但是当子进程调用exec后,因为exec运行新的程序后会覆盖从父进程继承来的存储映像,那么信号捕捉函数在新程序中无意义,所以exec会将原本设置要捕捉的信号都更改为默认动作。

    讨论

    fotk语句执行成功后,会返回一个子进程的pid给父进程,并且返回0给子进程,由于父子进程并发执行,所以这个时候x的值为1或者-1都有可能,看父子进程谁先被调用。

    尝试使用wait和waitid来同步父子进程的执行

    child_pid=wait(&stat_val);//父进程等待子进程的结束
    child_pid=wait(child_id,&stat_val,-1);//父进程等待子进程的结束

    什么是execl函数簇

    execl函数簇不是指一个函数,而是6个execl函数的统称,execl函数可以在不创建新进程的情况下,以一个全新的程序替换当前的程序的正文、数据和堆栈。

    在一个程序中有多少种信号,怎么使用一个用户定义信号

    #include
    #include
    #include
    #include
    
    void signal_handle(int sig_num){
    	if(sig_num==SIGUSR1)
    		printf("SIGUSR1-----\n");//在命令行中发送SIGUSR1信号可以执行signal_handle函数
    	printf("signal_handle\n");
    }
    
    int main(){
    	signal(SIGUSR1,signal_handle);
    	while(1){
    		printf("main------\n");
    		sleep(1);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在程序运行过程中,使用kill -s SIGUSR1 pid可以在命令行中发送对应的执行给程序,从而执行信号所对应的信号处理函数。

    signal和sigaction的区别是什么

    下面所指的signal都是指以前的older signal函数,现在大多系统都用sigaction重新实现了signal函数,不会出现空窗期。

    • signal在调用函数hander之前会有一段空窗期,在空窗期期间会先把hander指针恢复,这样会导致signal丢失信号,不能处理重复的信号;sigaction调用之后不会恢复hander指针,只有当再次调用sigaction修改handle指针才会恢复。

    调用signal()时,此函数的一个参数是函数指针。请描述函数指针的作用,并提供通过该技术处理更复杂场景的代码。你可以在网上搜索应用场景,也可以在网上引用代码。

    函数指针是指向函数的指针变量。它允许在程序运行时动态地引用和调用函数。函数指针非常有用,常用来做回调函数、动态函数调用等。
    在signal中的函数指针是用来做回调函数的,表示的是当指定的信号到来时,执行函数指针所指向的函数。(回调函数:函数指针可用于实现回调机制,其中可以将一个函数的指针传递给另一个函数,以便在某种事件发生时调用该函数。)
    比如:

    #include
    #include
    #include
    #include
    
    void signal_handle(int sig_num){
    	if(sig_num==SIGUSR1)
    		printf("SIGUSR1-----\n");//在命令行中发送SIGUSR1信号可以执行signal_handle函数
    	printf("signal_handle\n");
    }
    
    int main(){
    	signal(SIGUSR1,signal_handle);
    	while(1){
    		printf("main------\n");
    		sleep(1);
    	}
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
  • 相关阅读:
    【安全】网络安全态势感知
    单线圈无刷直流电机的电机驱动芯片GC1262E/S属于国产芯片可替代APX9262S/茂达
    [Spring]事务的传播机制
    MQ篇---第二篇
    KeyDB源码解析一——网络模型
    单例模式(Java实现)
    破局 NFT 困境:实用型 NFT 是什么?
    Xception:使用深度可分离卷积的深度学习算法
    科技云报道:混合办公的B面:安全与效率如何兼得?
    Solon2 常用注解之 @ProxyComponent 用法说明
  • 原文地址:https://blog.csdn.net/CodePlayMe/article/details/133907529