Bootstrap

3 进程

1、进程

  • 进程:是程序执行时的一个实例
    • 程序是被存储在磁盘上,包含机器指令和数据的文件
    • 当这些指令和数据被装载到内存并被CPU所执行,即形成了进程。
    • 一个程序可以被同时运行为多个进程
    • 在Linux源码中通常将进程称为任务(task)
    • 从内核观点看,进程的目的就是担当分配系统资源(CPU时间,内存等)的实体
  • 相关命令
    • pstree 以树状结构显示当前所有进程关系
    • ps 以简略方式显示当前用户拥有控制终端的进程信息,也可以配合以下选项
      a - 显示所有用户拥有控制终端的进程信息
      x - 也包括没有控制终端的进程
      u - 以详尽方式显示
      w - 以更大列宽显示

2、父子孤尸

  • Unix系统中的进程存在父子关系。一个父进程可以创建一到多个子进程,但每个子进程有且仅有一个父进程。整个系统中只有一个根进程,即PID为0的调度进程
    孤儿进程
  • 父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行,如果父进程先于子进程终止,子进程即成为孤儿进程
  • 同时被某个专门的进程收养,即成为该进程的子进程,因此该进程又被称为孤儿院进程
    僵尸进程
  • 父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行,如果子进程先于父进程终止,但由于某种原因,父进程并没有回收该子进程的终止状态,这时子进程即处于僵尸状态,被称为僵尸进程
  • 僵尸进程虽然已不再活动,即不会继续消耗处理机资源,但其所携带的进程终止状态会消耗内存资源。因此,作为程序的设计者,无论对子进程的终止状态是否感兴趣,都应该尽可能及时地回收子进程的僵尸

3、进程标识

  • 每个进程都有一个非负整数形式的唯一编号,即PID
  • PID在任何时刻都是唯一的,但是可以重用,当进程终止并被回收以后,其PID就可以为其它进程所用
  • 进程的PID由系统内核根据延迟重用算法生成,以确保新进程的PID不同于最近终止进程的PID
    系统中有些PID是专用的,比如
  • O号进程,调度进程,亦称交换进程(swapper),系统内核的一部分,所有进程的根进程,磁盘上没有它的可执行程序文件
  • 1号进程,init进程,在系统自举过程结束时由调度进程创建,读写与系统有关的初始化文件,引导系统至一个特定状态,以超级用户特权运行的普通进程,永不终止
  • 除调度进程以外,系统中的每个进程都有唯一的父进程,对任何一个子进程而言,其父进程的PID即是它的PPID
    相关函数
// 头文件 unistd.h
pid_t getpid(void); // 返回调用进程的PID
pid_t getppid(void); // 返回调用进程的父进程的PID
uid_t getuid(void); // 返回调用进程的实际用户ID
gid_t getgid(void); // 返回调用进程的实际组ID
uid_t geteuid(void); // 返回调用进程的有效用户ID
gid_t getegid(void); // 返回调用进程的有效组ID

4、创建子进程

  • 相关函数
    1:fork
//头文件 unistd.h
pid_t fork(void);
- 功能:创建调用进程的子进程
- 返回值:成功分别在父子进程中返回子边程的PID和0,失败返回-1- 注意!!!该函数调用一次返回两次,在父进程中返回所创建子进程的PID,,而在子进程中返回0,函数的调用者可以根据返回值的不同,分别为父子进程编写不同的处理分支
- 系统中总的线程数达到了上线,或者用户的总进程数达到了上线,fork函数会返回失败。
	- 线程上线:/proc/sys/kernel/threads-max
	- 进程上线:ulimit-u

注意:fork之后的代码块,父子进程都需要执行

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

int main(){
	printf("%d进程:我是父进程,要创建儿子\n",getpid());
	pid_t a = fork();// 创建子进程 ,此时为父进程返回子进程的id,而给子进程返回0
	if(a== -1){ 
			perror("fork");
			return -1;
	}
	printf("%d进程:zpyl\n",getpid());
	return -1;
}

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

  • 思考
for(int i=0;i<3;i++){
	fork();
}
// 问一共有多少个进程  
// ---8个

5、进程间关系

由fork产生的子进程是其父进程的不完全副本,子进程在内存中的映像除了代码区与父进程共享同一块物理内存,其它各区映射到独立的物理内存,但其内容从父进程拷贝。
代码区:可执行指令、字面值常量、具有常属性且被初始化的全局和静态局部变量
在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int global = 10;// 数据区
int main(){
	int* heap = malloc(sizeof(int));// 堆区
	*heap = 20;
	int local = 30;// 栈区
	printf("%d进程,%p:%d %p:%d %p:%d\n",getpid(),&global,global,heap,*heap,&local,local);
	// 创建子进程
	pid_t pid = fork();
	if(pid==-1){
		perror("fork");
		return -1;
	}
	// 子进程代码
	if(pid==0){
		printf("%d进程,%p:%d %p:%d %p:%d\n",getpid(),&global,++global,heap,++*heap,&local,++local);
		return 0;
	}
	// 父进程代码
	else{
		sleep(1);// 子进程创建是需要时间,所以这里进行了等待
		printf("%d进程,%p:%d %p:%d %p:%d\n",getpid(),&global,global,heap,*heap,&local,local);
		return 0;
	}
}
  • 结果
    在这里插入图片描述

说明子进程是父进程的副本,连虚拟内存地址都进行了复制

  • fork函数返回后,系统内核会将父进程维护的文件描述符表也复制到子进程的进程表项中,但并不复制文件表项
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(){
	// 打开文件
	int fd= open("./open.txt",O_RDWR|O_CREAT|O_TRUNC,0,0);
	if(fd==-1){
		perror("open");
		return -1;
	}
	char* buf = "hello world";
	if(write(fd,buf,strlen(buf))==-1){
		perror("write");
		return -1;
	}
	pid_t pid = fork();
	if(pid == 0){
		lseek(fd,-6,SEEK_END);
		return -1;
	}
	else{
		sleep(1);
		buf = "linux!";
		if(write(fd,buf,strlen(buf))==-1){
			perror("write");
			return -1;
		}
	}
	return 0;
} 
  • 孤儿进程演示
#include <stdio.h>
#include <unistd.h>

int main(){
	// 创建子进程
	pid_t pid = fork();
	if(pid ==-1){
		perror("fork");
		return -1;
	}
	// 子进程代码
	if(pid==0){
		printf("%d进程:我的亲爹是%d\n",getpid(),getppid());
		sleep(2);
		// 此时父进程已经死了,此进程由孤儿院进程收养
		printf("%d进程:我的新爹是%d\n",getpid(),getppid());
		return 0;
	}else{
		sleep(1);
		return 0;
	}
}

在这里插入图片描述

  • 僵尸进程演示
#include <stdio.h>
#include <unistd.h>
int main(){
	// 创建子进程
	pid_t pid = fork();
	if(pid == -1){
		perror("fork");
		return -1;
	}
	// 子进程代码
	if(pid == 0){
		printf("%d进程:我是子进程,马上死,变僵尸\n",getpid());
		return 0;
	}else{
		// 父进程代码
		printf("%d进程:我是父进程我很忙\n",getpid());
		sleep(15);
		return 0;
	}
}

在这里插入图片描述

在这里插入图片描述

6、进程的终止

6.1 正常终止

进程正常终止分为三种情况:
(1) 从main函数中返回可令进程正常终止
(2) 调用exit函数令进程中止

// 头文件 stdlib.h
void exit(int status);
- 功能:令进程终止
- 参数:status 进程的退出码,相当main函数的返回值

虽然exit函数的参数和main函数的返回值都是int类型,但只有其中最低数位的字节可被其父进程回收,高三个字节会被忽略,因此在设计进程的退出码时最好不要超过一字节的值域范围

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int fun(){
	printf("%d进程:我是fun\n",getpid());
	exit(0);
	return 10;
}
int main(){
	printf("%d进程:fun函数返回%d\n",getpid(),fun());
	return 0;
}

exit函数在终止调用进程之前还会做几件收尾工作
A、调用实现通过atexit或on_exit函数注册的退出处理函数;
B、冲刷并关闭所有仍处于打开状态的标准I/O流;
C、删除所有通过tmpfile函数创建的临时文件;
D、_exit(status)

  • 注册退出处理函数
    1:atexit
// 头文件 stdlib.h
int atexit (void (* function)(void));
- 参数:function函数指针,指向退出处理函数
- 返回值:成功返回0,失败返回-1

注意atexit函数本身并不调用退出处理函数,而只是将function参数所表示的退出处理函数地址,保存(注册)在系统内核的某个地方(进程表项)。待到exit函数被调用或在main函数里执行return语句时,再由系统内核根据这些退出处理函数的地址来调用它们。此过程亦称回调
2:on_exit

// 头文件 stdlib.h
int on_exit (void (* function)(int,void* ),void* arg);
- 参数:
	- function 函数指针,指向退出处理函数。其中第一个参数来自传递给exit函数的status参数或在main函数里执行return语句的返回值,而第二个参数则来自传递给on_exiti函数的arg参数
	- arg 泛型指针,将作为第二个参数传递给function所指向的退出处理函数
- 返回值:成功返回0,失败返回-1
  • 案例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 退出处理函数
void exitfun(){
    printf("我是退出处理函数\n");
}
void exitfun1(int status,void* arg){
    printf("exitfun1被调用,status:%d arg:%s\n",status,(char* )arg);
}
int fun(){
	printf("%d进程:我是fun\n",getpid());
	exit(0);
	return 10;
}
int main(){
	// 注册退出处理函数
	atexit(exitfun); 
	on_exit(exitfun1,"zpyl"); // 退出函数的调用顺序是先注册的后执行
	printf("%d进程:fun函数返回%d\n",getpid(),fun());
	return 0;
}

(3)调用_exit/_Exit 函数令进程终止

// _exit
// 头文件 unistd.h
void _exit(int status);
- 参数:status进程退出码,相当于main函数的返回值
// _Exit
// 头文件 stdlib.h
void _Exit(int status);
- 参数:status进程退出码,相当于main函数的返回值

直接调用_exit和_Exit时,并不会执行退出处理函数等,_exit在终止调用进程之前也会做几件收尾工作,但与exit函数所做的不同。事实上,exit函数在做完它那三件收尾工作之后紧接着就会调用_exit函数
A、关闭所有仍处于打开状态的文件描述符
B、将调用进程的所有子进程托付给init进程收养
C、向调用进程的父进程发送SIGCHLD(17)信号
D、令调用进程终止运行,将status的低八位作为退出码保存在其终止状态中

6.2 异常终止

1、当进程执行了某些在系统看来具有危险性的操作,或系统本身发生了某种故障或意外,内核会向相关进程发送特定的信号。如果进程无意针对收到的信号采取补救措施,那么内核将按照缺省方式将进程杀死,并视情形生成核心转储文件(core)以备事后分析,俗称吐核
SIGLL(4):进程试图执行非法指令
SIGBUS(7):硬件或对齐错误
SIGFPE(8):浮点异常
SIGSEGV(11):无效内存访问
SIGPWR(30):系统供电不足
2、人为触发信号
SIGINT(2):Ctrl+C
SIGQUIT(3):Ctrl+
SIGKILL(9):不能被捕获或忽略的进程终上信号
SIGTERM(15):可以被捕获或忽略的进程终止信号
3、向进程自己发送信号
头文件 stdlib.h
void abort(void);

  • 功能:向调用进程发送SIGABRT(6)信号,该信号默认情况下可使进程结束

7、回收子进程

  • 清除僵尸进程,避免消耗系统资源
  • 父进程需要等待子进程的终止,以继续后续工作
  • 父进程需要知道子进程终止的原因
    • 如果是正常终止,那么进程的退出码是多少
    • 如果是异常终止,那么进程是被哪个信号所终止的
      相关函数
      1:wait
// 头文件 sys/wait.h
pid_t wait(int* status);
- 功能:等待并回收任意子进程
- 参数:status用于输出子进程的终止状态,可置NULL
- 返回值:成功返回所回收的子进程的PID,失败返回-1

父进程在创建若干子进程以后调用wait函数:
A.若所有子进程都在运行,则阻塞,直到有子进程终止才返回;
B.若有一个子进程已终止,则返回该子进程的PID并通过status参数输出其终止状态;
C.若没有任何可被等待并回收的子进程,返回-1,置errno为ECHILD。
子进程的终止状态通过wait函数的status参数输出给该函数调用者。在<sys/wait.h>头文件提供了几个辅助分析进程终止状态的工具宏
WIFEXITED(status)
真:正常终止 WEXITSTATUS(status) - 进程退出码
假:异常终止 WTERMSIG(status) - 终止进程的信号
WIFSIGNALED(status)
真:异常终止 WTERMSIG(status) - 终止进程的信号
假:正常终止 WEXITSTATUS(status) - 进程退出码

#include <stdio.h>
#include <stdlib.h> // exit
#include <unistd.h> // fork
#include <sys/wait.h> // wait
int main(){
	// 创建子进程
	pid_t pid = fork();
	if(pid == -1){
		perror("fork");
		return -1;
	}
	// 子进程代码,暂时不结束
	if(pid==0){
		printf("%d进程:我是子进程,暂时不结束\n",getpid());
		//sleep(5);
		//int *p=NULL;*p=100; // 触发核心已转储
		abort();// 自己调用abort来异常结束
		return 0;
	}
	// 父进程代码,收尸
	int s ;// 用来输出子进程的终止转态
	pid_t childpid = wait(&s);
	if(childpid == -1){
		perror("wait");
		return -1;
	}
	printf("%d进程:回收了%d进程的僵尸\n",getpid(),childpid);
	if(WIFEXITED(s)){
		printf("正常结束:%d\n",WEXITSTATUS(s));
	}else{
		printf("异常结束:%d\n",WTERMSIG(s));
	}
	return 0;
}
  • wait回收子进程
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<errno.h>
int main(void){
    //创建多个子进程
    for(int i = 0;i < 5;i++){
        pid_t pid = fork();
        if(pid == -1){
            perror("fork");
            return -1;
        }
        if(pid == 0){
            printf("%d进程:我是子进程\n",getpid());
            sleep(i + 1);
            return 0;
        }
    }
    //回收多个子进程
    for(;;){
        pid_t pid = wait(NULL);
        if(pid == -1){
            if(errno == ECHILD){
                printf("没有子进程了\n");
                break;
            }else{
                perror("wait");
                return -1;
            }
        }
        printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
    }
    return 0;
}

2:waitpid

// 头文件 sys/wait.h
pid_t waitpid(pid_t pid,int* status,int options);
- 功能:等待并回收任意或特定子进程
- 参数:
	- pid 可以以下取值
		-1 等待并回收任意子进程,相当于wait函数
		>0 等待并回收特定子进程
	- status用于输出子进程的终止状态,可置NULL
	- options可以如下取值
		0 阻塞模式,若所等子进程仍在运行,则阻塞直至其终止。 
		WNOHANG 非阻塞模式,若所等子进程仍在运行,则返回0
	- 返回值:成功返回所回收子进程的PID或者0 失败返回-1
// waitpid(-l,&status,0)等价于wait(&status);
  • 案例:以非阻塞的方式回收僵尸进程
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(){
	// 创建子进程
	pid_t pid = fork();
	if(pid==-1){
		perror("fork");
		return -1;
	}
	// 子进程代码,暂时不结束
	if(pid==0){
		printf("%d进程:我是子进程,暂时不结束\n",getpid());
		sleep(5);
		return 0;
	}
	// 父进程代码 非阻塞收尸
	for(;;){
		pid_t childpid = waitpid(pid,NULL,WNOHANG);
		if(childpid == -1){
			perror("waitpid");
			return -1;
		}else if(childpid == 0){
			printf("%d进程,子进程在运行,干其他事情\n",getpid());
			sleep(1);
		}else{
			printf("%d进程:回收了%d进程的僵尸\n",getpid(),childpid);
			break;
		}
	}
	return 0;
}
  • 事实上,无论一个进程是正常终止还是异常终止,都会通过系统内核向其父进程发送SIGCHLD(17)信号。父进程可以忽略该信号,也可以提供一个针对该信号的信号处理函数,在信号处理函数中以异步的方式回收子进程。这样做不仅流程简单,而且僵尸的存活时间短,回收效率高

8、创建新进程

  • 与fork不同,exec函数不是创建调用进程的子进程,而是创建一个新的进程取代调用进程自身。新进程会用自己的全部地址空间,覆盖调用进程的地址空间,但进程的PID保持不变
  • exec不是一个函数而是一堆函数(共6个),一般称为exec函数族。它们的功能是一样的,用法也很相近,只是参数的形式和数量略有不同
// 头文件 unistd.h
int execl (const char* path,const char* arg,...)
int execlp (const char* file,const char* arg,...)// path环境变量
int execle (const char* path,const char* arg,...,char* const envp[]); //envp 环境变量
int execv(const char* path,char* const argv[]);
int execvp(const char* file,char* const argv[]);
int execve (const char* path,char* const argv[],char* const envp[]);

在这里插入图片描述

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

int main(void){
    printf("%d进程:我要变身了\n",getpid());

    /*if(execl("/bin/ls","ls","-l","-i",NULL) == -1){
        perror("execl");
        return -1;
    }*/
    /*if(execlp("ls","ls","-l",NULL) == -1){
        perror("execlp");
        return -1;
    }*/
    char* envp[4] = {"NAME=laozhang","AGE=18","FOOD=锅包肉",NULL};
    /*if(execle("./new","./new","hello",NULL,envp) == -1){  // 这里的环境变量也可以用于旧进程向新进程传递的数据
        perror("execle");
        return -1;
    }*/
    char* argv[] = {"./new","hello","123",NULL};
    if(execve("./new",argv,envp) == -1){
        perror("execle");
        return -1;
    }

    printf("%d进程:我变身完成了\n",getpid()); // 这里是不会有打印的,这是因为此时旧进程已经被新进程替代了
    return 0;
}

调用exec函数不仅改变调用进程的地址空间和进程映像,调用进程的一些属性也发生了变化

  • 任何处于阻塞状态的信号都会丢失
  • 被设置为捕获的信号会还原为默认操作
  • 有关线程属性的设置会还原为缺省值
  • 有关进程的统计信息会复位
  • 与进程内存相关的任何数据都会丢失,包括内存映射文件
  • 标准库在用户空间维护的一切数据结构(如通过atexit或on_exit函数注册的退出处理函数)都会丢失
  • 有些属性会被新进程继承下来,比如PID、PPID、实际用户ID和实际组ID、优先级,以及文件描述符等
    调用exec函数固然可以创建出新的进程,但是新进程会取代原来的进程。如果既想创建新的进程,同时又希望原来的进程继续存在,则可以考虑fork+exec模式,即在fork产生的子进程里调用exec函数,新进程取代了子进程,但父进程依然存在
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(){
	pid_t pid= fork();
	if(pid==-1){
		perror("fork");
		return -1;
	}
	if(pid == 0){
		if(execl("/bin/ls","ls","-l",NULL)==-1){
			perror("execl");
			return -1;
		}
	}
	printf("父进程执行\n");
	// 收尸
	int status;
	if(waitpid(pid,&status,0)==-1){
		perror("waitpid");
		return -1;
	}
	if(WIFEXITED(status)){
		printf("正常结束:%d\n",WEXITSTATUS(status));
	}else{
		printf("异常结束:%d\n",WTERMSIG(status));
	}
	return 0;
}

9、system

// 头文件 stdlib.h
int system (const char* command);
- 功能:执行shell命令
- 参数:command shell命令行字符串
- 返回值:成功返回command进程的终止状态,失败返回-1
- system函数执行command参数所表示的命令行,并返回命令进程的终止状态
- 若command参数取NULL,返回非0表示Shell可用,返回O表示Shell不可用

在system函数内部调用了vfork、exec和waitpid等函数

  • 如果调用vfork或waitpid函数出错,则返回-1
  • 如果调用exec函数出错,则在子进程中执行exit(127)
  • 如果都成功,则返回command进程的终止状态(由waitpid的status参数获得)
    使用system函数而不用vfork+exec的好处是,systemi函数针对各种错误和信号都做了必要的处理,而且system是标准库函数,可跨平台使用
    vfork与fork的区别,fork创建出来的进程是复制父进程的数据,而vfork创建出来的进程和父进程共用数据,而不是复制
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(){
	int s = system("ls -l");
	if(s==-1){
		perror("system");
		return -1;
	}
	if(WIFEXITED(s)){
		printf("正常终止:%d\n",WEXITSTATUS(s));
	}else{
		printf("异常终止:%d\n",WTERMSIG(s));
	}
	return 0;
}
;