Linux环境编程-进程管理

发布时间 2023-09-09 21:26:20作者: 冲他丫的

一、进程的基本概念

1、进程与程序

程序是存储在磁盘上的可执行文件,程序被加载到内存中开始运行称为进程,一个程序可以同时加载成多个进程,进程就是处于活动状态下的程序

2、进程的分类

进程根据功能不同一般分为三种类型:交互进程、批处理进程、守护进程

  • 交互进程:由一个shell终端启动的进程,在运行过程中需要与用户进行交互操作,可以运行于前台,也可以在后台运行

  • 批处理进程:该进程是一个进程的集合,负责按顺序执行预定义好的操作

  • 守护进程:一般处于活跃状态,运行在后台,一般都是由系统在开机时通过启动系统脚本自动创建

3、查看进程

简单形式:ps  显示当前用户有控制终端的进程简单信息

列表形式:ps -auxw 以最大列宽显示所有进程的详细信息

     USER        进程的属主     
      PID         进程的编号
      %CPU        CPU的使用率
      %MEM        内存的使用率 
      VSZ         虚拟内存使用的字节数   
      RSS         物理内存使用的字节数
      TTY         终端设备号 ?表示无终端控制  
      STAT      进程的状态
                  O   就绪态 等待被调度
                  R   运行态 Linux没有O,就绪也用R表示
                  S   可被唤醒的睡眠态,例如系统中断、获得资源、收到信号都可以唤醒进入R
                  D   不可被唤醒的睡眠态,只能被系统唤醒
                  T   暂停态 收到SIGSTOP转入T,收到SIGCONT转入R
                  Z   僵尸态
                  X   死亡状态
                  <   高优先级 
                  N   低优先级 
                  s   进程组中的领导者        
                  l   多线程的进程
                  +   处于后台的进程组
      START TIME  进程运行总时间
      COMMAND 进程的启动命令          

4、父进程、子进程、孤儿进程、僵尸进程

一个进程可以被另一个进程创建,创建者称为被创建者的父进程,被创建者称为创建者的子进程,当子进程被父进程创建启动后在操作系统的调度下同时运行

当子进程先于父进程结束时,子进程会向父进程发送SIGCHLD信号,此时父进程应该去回收子进程的相关资源,如果父进程没有去回收子进程,那么子进程变成了僵尸进程

父进程先于子进程结束,子进程就变成孤儿进程,所有的孤儿进程都会被系统指定的一个进程领养(1 /sbin/init)接管,该进程就变成了孤儿进程的父进程

5、进程标识符 pid_t

每个进程都有一个以非负整数表示的唯一标识,即进程ID\PID

进程ID在任意时刻中都是唯一的,但是可以重用,进程一旦结束它的进程ID会被系统回收,过一段时间后,可以继续分配给新的进程(延时重用)

  • getpid: 当前进程
pid_t getpid(void);
        功能:获取调用者的进程ID
  • getppid:        父进程
pid_t getppid(void);
        功能:获取调用者父进程的进程ID

二、进程的创建fork/vfork

fork

pid_t fork(void);
          功能:创建一个子进程
          返回值:成功时一次调用两次返回,子进程返回0,父进程返回的是子进程的pid
              当进程的数量超过系统的限制可能会创建失败返回一次-1       

实例:

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

int main(int argc,const char* argv[])
{
    printf("我是进程%u\n",getpid());
    printf("我要创建子进程了\n");
    printf("***");
    pid_t pid = fork();
    if(0 == pid)
    {
        printf("我是子进程%u,我的父进程是%u\n",getpid(),getppid());
    }
    else
    {
        printf("我是父进程%u,我的子进程是%u\n",getpid(),pid);
    }
}

注意

1、子进程创建成功后,会与父进程同样从fork处开始继续往下执行,为了让父子进程分别执行不同的功能,可以通过返回值的不同让它们进入不同的分支语句

2、通过fork创建的子进程会拷贝父进程的数据段、bss段、堆、栈、I/O流缓冲区等数据,与父进程共享代码段的数据,子进程会继承父进程的信号处理方式

3、fork结束后,不确定是父进程先返回还是子进程先返回,可以通过休眠函数来让另一个进程先执行

4、通过fork创建的子进程可以共享父进程的文件描述符

练习1:通过fork创建4个子进程,再为这4个子进程各自创建2个子进程

2的三次方

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

int main(int argc,const char* argv[])
{
    printf("*");
    for(int i=0; i<3; i++)
    {
        fork();    
    }
    printf("%u\n",getpid());

    for(;;)
    {
        fork();    
    }
}

vfork

pid_t vfork(void);
    功能:以加载可执行程序的方式创建并运行子进程
    返回值:成功子进程返回0 父进程返回子进程pid

注意

1、子进程一定先返回,此时子进程不算完全创建成功,需要加载一个可执行程序来替换当前子进程的所有资源,替换完成后子进程才算完全的创建成功,此时父进程才会返回

exec系列函数

使用exec系列函数让子进程加载可执行程序

  • execl
int execl(const char *path, const char *arg, .../* (char  *) NULL */);
    path:要加载的可执行文件的路径
    arg:命令行参数 最后一个一定要以NULL结尾
  • execlp
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
    file:要加载的可执行文件的名字,会根据PATH环境变量中记录的路径查找并加载该文件
    注意:如果想要指定一个路径,那么需要在配置文件中增加PATH的值

注意:如果想要指定一个路径,那么需要在配置文件中增加PATH的值

  • execle
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
    path:要加载的可执行文件的路径
    arg:命令行参数 最后一个一定要以NULL结尾
    envp:环境变量表
  • execv/execvp/execvpe
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系列函数成功不返回,失败才会返回-1

2、以vfork、exec函数创建出来的子进程不会继承父进程的信号处理方式,但是能继承父进程的信号屏蔽集

  • system
int system(const char *command);
    功能:创建子进程加载command程序来执行系统命令

考点:对比fork和vfork区别?

  • fork()子进程拷贝父进程的数据段和代码段,这里通过拷贝页表实现。这样得到的子进程独立于父进程,具有良好的并发性
    vfork()子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,子进程对该地址空间中任何数据的修改同样为父进程所见无需拷贝页表,效率更高。

  • fork()父子进程的执行次序不确定。使用fork创建一个子进程是哪个进程先运行取决于系统的调度算法
    vfork()保证子进程先运行,在调用 exec 或 exit
    之前与父进程数据是共享的。父进程在子进程调用 exec 或 exit 之后才可能被调度运行,如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁

三、进程的正常退出exit

进程正常退出前要完成步骤:

1、先调用atexit\on_exit函数进行注册的函数,如果注册多个函数,结束时这些函数的执行顺序与注册顺序相反

2、冲刷并关闭所有打开状态下的标准IO流

3、调用_exit_Exit函数

正常退出的方法

1、执行了main函数的return n语句,该返回值可以被父进程接收到

2、进程调用了exit函数,该函数是属于C标准库提供的

  • exit
void exit(int status);
        功能:能在任意时候结束进程
        status:结束状态码EXIT_SUCCESS\EXIT_FAILURE,效果与main函数return 的值等同
        注意:该函数不会返回
  • atexit
int atexit(void (*function)(void));
        功能:向内核注册绑定一个进程结束前要执行的函数
  • on_exit
int on_exit(void (*function)(int,void *),void *arg);
          功能:向内核注册绑定一个进程结束前要执行的函数
          arg:会在调用function的时候传递给该函数

实例:

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

void at_exit_fp(void)
{
    printf("我要死了!\n");    
}

void on_exit_fp(int num,void* arg)
{

    if(EXIT_SUCCESS == num)
    {
        printf("num=%d arg=%s\n",num,(char*)arg);    
    }
    else
    {
        printf("有人杀了我\n");    
    }
}

int main(int argc,const char* argv[])
{
    atexit(at_exit_fp);
    on_exit(on_exit_fp,"你死吧");
    printf("我还活着\n");
    sleep(3);
    //exit(EXIT_SUCCESS);
    return EXIT_FAILURE;
}
输出:
我还活着
有人杀了我
我要死了!

3、调用_exit_Exit函数

  • _exit
void _exit(int status);
        功能:结束进程,由系统提供
  • _Exit
void _Exit(int status);
        功能:结束进程,由标准库提供

①它们的参数status会被父进程获取到

②结束前会关闭所有打开状态下的系统IO的文件描述符

③向父进程发出结束信号SIGCHLD

④该函数不会返回

4、进程的最后一个线程执行了return返回语句

5、进程的最后一个线程执行了pthread_exit函数

四、进程的异常终止

1、进程调用了abort函数,产生了SIGABRT信号

2、进程接收到了其他进程的某些信号,导致终止

3、进程自己的错误操作导致终止,例如:段错误、除零等

4、进程的最后一个线程接收到了"取消"请求操作,并响应了请求

注意:以上的异常终止方式,都会让父进程无法获取结束进程的结束状态码,所以才叫异常终止

注意:无论进程是如何结束的,他们的资源都会被回收,所有的文件描述符、文件指针都会被关闭

五、子进程的回收wait/waitpid

对于任何结束方式,都希望父进程能够获取到,通过wait、waitpid函数可以知道子进程是如何结束的以及结束状态码

  • wait

wait()会暂停当前进程的执行,直到有信号到来或者子进程结束。总的来说,wait()的作用就是阻塞父进程,等待子进程。

pid_t wait(int *status);
      功能:等待任意子进程的结束,并获取其结束状态码
      status:输出型参数,用来保存子进程退出时的状态;
      返回值:子进程的ID
          1、如果所有子进程都还运行,则阻塞等待
          2、如果有一个子进程结束,立即返回该进程的结束状态码和pid
          3、如果没有子进程运行,立即返回-1
      WIFEXITED(status)
          判断子进程是否是正常退出,如果是返回真
      WEXITSTATUS(status)
          如果子进程是正常退出的,可以获取正确的结束状态码,只获取低八位
      WIFSIGNALED(status)
         判断子进程是否是异常退出,如果是返回真 
      WTERMSIG(status)
          如果子进程是异常退出的,可以获取杀该子进程的信号id

实例:

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

void sigchld(int num)
{
    //    当子进程结束时,父进程会收到SIGCHLD信号
    int status = -100;
    pid_t pid = wait(&status);
    if(WIFEXITED(status))
    {
        printf("子进程%u是正常结束的,结束状态码是%d\n",pid,WEXITSTATUS(status));    
    }
    else
    {
        printf("子进程%u是异常结束,死亡信号是%d\n",pid,WTERMSIG(status));
    }
}

int main(int argc,const char* argv[])
{
    signal(SIGCHLD,sigchld);

    for(int i=0; i<10; i++)
    {
        if(0 == fork())
        {
            printf("我是第%d个子进程%u\n",i,getpid());
            sleep(rand()%10+3);
            return 0;
        }
    }

    for(;;)
    {
        //    父进程执行自己的业务逻辑 不会受到wait阻塞的影响
        printf("*");
        fflush(stdout);
        sleep(1);
    }
}
  • waitpid
pid_t waitpid(pid_t pid, int *status, int options);
      功能:指定回收某个或者某些子进程的状态
      pid:
          <-1     等待组id为abs(pid)的进程组中的任意子进程结束
          -1      等待任意子进程结束 功能与wait等同
          0       等待调用者同组的任意子进程结束
          >0      等待该进程结束
      status:处理与wait等同
      options:等待模式
          0       正常的阻塞模式 等同wait
          WNOHANG 非阻塞模式 如果没有子进程结束,立即返回0
          WUNTRACED 如果等待的子进程中有处于暂停态的,立即返回该子进程的状态码
          WCONTINUED 如果等待的子进程中有从暂停态转入为运行的,立即返回该子进程的状态码

          用于判断和获取status的宏函数:
          WIFSTOPPED 判断状态码,如果子进程是转入暂停态,返回真
          WSTOPSIG   当子进程是暂停态返回时,获取导致暂停态的信号id
          WIFCONTINUED 判断状态码,如果子进程是由暂停态转入运行态,返回真      

实例:

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


int main(int argc,const char* argv[])
{
    for(int i=0; i<10; i++)
    {
        if(0 == fork())
        {
            printf("我是第%d个子进程%u\n",i,getpid());
            sleep(rand()%10+3);
            if(i%2)
            {
                kill(getpid(),19);
            }
            return -1;
        }
    }

    for(;;)
    {
        //    父进程执行自己的业务逻辑 不会受到wait阻塞的影响
        printf("*");
        fflush(stdout);
        sleep(1);
        int status = -100;
        pid_t pid = waitpid(-1,&status,WUNTRACED|WNOHANG);
        if(0 != pid)
        {
            if(WIFEXITED(status))
            {
                printf("子进程%u是正常结束的,结束状态码是%d\n",pid,WEXITSTATUS(status));    
            }
            else if(WIFSTOPPED(status))
            {
                printf("子进程%u是转入了暂停态,暂停信号是%d\n",pid,WSTOPSIG(status));
            }
            else
            {
                printf("子进程%u是异常结束,死亡信号是%d\n",pid,WTERMSIG(status));
            }
        }
    }
}


注意:

1、如果没有子进程结束 wait会阻塞等待,如果在父进程的业务逻辑代码出wait,会对父进程的业务产生阻塞影响,因为子进程结束时会给父进程发送SIGCHLD信号,可以通过在父进程中注册该信号处理函数,在该函数内执行wait,就不影响父进程的业务执行,但是信号最好选择可靠信号

2、waitpid可以选择不阻塞等待,因此不需要像wait一样在信号处理函数中执行,可以直接在父进程的业务逻辑中调用不受影响

练习:运用今天的知识,实现一个system命令的功能

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

int _system(const char* command)
{
	int size = 1;
	const char* base = command;
	//	计算命令参数的个数
	while(strstr(command," "))
	{
		size++;	
		command = strstr(command," ")+1;
	}

	//	解析命令参数
	char* cmd[size+1];
	for(int i=0; i<size; i++)
	{
		cmd[i] = malloc(20);
		sscanf(base,"%s ",cmd[i]);
		base = strstr(base," ")+1;
		printf("%s\n",cmdi[i]);
	}
	cmd[size] = NULL;
	
	pid_t pid = vfork();
	if(0 == pid)
	{
		return execvp(cmd[0],cmd);	
	}
	else
	{
		int status = 0;
		waitpid(pid,&status,0);
		for(int i=0; i<size; i++) free(cmd[i]);
		return status;
	}
}

int main(int argc,const char* argv[])
{
	_system("ls -l -a");	
	printf("heheh\n");
}