Linux学习八 PCB

972 阅读11分钟

进程控制块PCB

在Linux中就是一个结构体:task_struct

位置:/usr/src/linux-headers-4.4.0-142/include/linux/sched.h

task_struct结构体的内部成员很多:

* 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
* 进程的状态,有就绪、运行、挂起、停止等状态。
* 进程切换时需要保存和恢复的一些CPU寄存器。
* 描述虚拟地址空间的信息。
* 描述控制终端的信息。
* 当前工作目录(Current Working Directory)。
* umask掩码。
* 文件描述符表,包含很多指向file结构体的指针。
* 和信号相关的信息。
* 用户id和组id。
* 会话(Session)和进程组。
* 进程可以使用的资源上限(Resource Limit)。





环境变量

存储形式:与命令行参数类似。char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。

打印环境变量表:

#include <stdio.h>

//引入环境变量表
extern char **environ;

int main(void)
{
    int i;

    //第二个参数等价于environ[i]!=NULL
    for (i = 0; environ[i]; i++) {
        printf("%s\n", environ[i]);
    }

    return 0;
}



getenv 获取指定环境变量的值

setenv 设置或者添加环境变量的值

unsetenv 删除环境变量的值

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

int main(void)
{
	char *val;
	const char *name = "ABD";

	val = getenv(name);
	printf("1, %s = %s\n", name, val);

	setenv(name, "haha-day-and-night", 1);

	val = getenv(name);
	printf("2, %s = %s\n", name, val);

#if 1 //这个表示这段代码执行,下面的else不执行
	int ret = unsetenv("ABD=");
    printf("ret = %d\n", ret);

	val = getenv(name);
	printf("3, %s = %s\n", name, val);

#else
	int ret = unsetenv("ABD");  //name=value:value
	printf("ret = %d\n", ret);

	val = getenv(name);
	printf("3, %s = %s\n", name, val);

#endif

	return 0;
}






fork

作用:创建单个子进程

fork的返回值:

On success, the PID of the child process is returned in the parent, and 0 is returned in the child.

也就是说,如果fork成功的话,那么在父进程中这个调用会返回子进程的pid,在子进程中会返回0

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

int var = 34;

int main(void)
{
    pid_t pid;
    
    pid = fork();
    if (pid == -1 ) {
        perror("fork");
        exit(1);
    } else if (pid > 0) {
        sleep(2);
        var = 55;
        printf("I'm parent pid = %d, parentID = %d, var = %d\n", getpid(), getppid(), var);
    } else if (pid == 0) {
        var = 100;
        printf("child  pid = %d, parentID=%d, var = %d\n", getpid(), getppid(), var);
    }
    printf("var = %d\n", var);
    
    return 0;
}


输出结果:

child  pid = 20534, parentID=20524, var = 100
var = 100
I'm parent pid = 20524, parentID = 20525, var = 55
var = 55
Program ended with exit code: 0





循环创建子进程

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


int main(void)
{
    int i;
    pid_t pid;
    printf("xxxxxxxxxxx\n");

    for (i = 0; i < 5; i++) {
        pid = fork();
        if (pid == 0) {
            //注意这儿的break,子进程在创建完成之后需,要让子进程跳出循环
            break;
        }
    }

    if (i < 5) {

        //这儿的sleep都是为了输出好看用的
        sleep(i);
        printf("I'am %d child , pid = %u\n", i+1, getpid());

    } else  {
        sleep(i);
        printf("I'm parent\n");
    }

    return 0;
}





getpid

获取当前进程id

pid_t getpid(void);



getppid

获取当前进程的父进程id

pid_t getpid(void);

实际用户ID(实际组ID):标识当前用户(所属组)是谁,当用户登陆时取自口令文件。

有效用户ID(有效组ID):用来决定我们(当前进程)对文件的访问权(即实际该进程的是以那个用户运行的)。





getuid

uid_t getuid(void);//获取当前进程实际用户id

uid_t geteuid(void);//后去当前进程的有效游湖id





getgid

gid_t getgid(void);//获取当前进程的实际用户组id

gid_t getegid(void);//获取当前进程的有效用户组id



进程共享

父子进程之间在fork后。有哪些相同,那些相异之处呢?

刚fork之后:

  • 父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...
  • 父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集

似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗? 当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。

重点注意!躲避父子进程共享全局变量的知识误区!

【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区 (进程间通信详解)

特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。



gdb调试子进程

使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。

  • set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程。
  • set follow-fork-mode parent 设置跟踪父进程。

注意,一定要在fork函数调用之前设置才有效。





exec函数族

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

其实有六种以exec开头的函数,统称exec函数:
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 execve(const char *path, char *const argv[], char *const envp[]);





execlp函数

l代表的是list;p代表的是path

加载一个程序,借助PATH环境变量

int execlp(const char *file, const char *arg, ...); 成功:无返回;失败:-1

  • 参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。
  • 该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。





execl函数

加载一个进程, 通过 路径+程序名 来加载。

int execl(const char *path, const char *arg, ...); 成功:无返回;失败:-1

execlp("ls", "ls", "-l", "-F", NULL);	     使用程序名在PATH中搜索。
execl("/bin/ls", "ls", "-l", "-F", NULL);    使用参数1给出的绝对路径搜索。





execvp函数

加载一个进程,使用自定义环境变量env

int execvp(const char *file, const char *argv[]); 变参形式: ①... ② argv[] (main函数也是变参函数,形式上等同于 int main(int argc, char *argv0, ...))

变参终止条件:① NULL结尾 ② 固参指定 execvp与execlp参数形式不同,原理一致。





exec函数族一般规律

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

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

事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。

用法:

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

int main(int argc, char *argv[])
{
    printf("========================\n");

    char *argvv[] = {"ls", "-l", "-F", "R", "-a", NULL};
    

    pid_t pid = fork();
    if (pid == 0) {
        execl("/bin/ls", "ls", "-l", "-F", "-a", NULL);
        execv("/bin/ls", argvv);
        perror("execlp");
        exit(1);

    } else if (pid > 0) {
        sleep(1);
        printf("parent\n");
    }


    return 0;
}

编写程序实现ps的功能:查看正在运行的进程

提示:dup2、exec

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

int main(void)
{
	int fd;

	fd = open("ps.out", O_WRONLY|O_CREAT|O_TRUNC, 0644);
	if(fd < 0){
		perror("open ps.out error");
		exit(1);
	}
    
    dup2(fd, STDOUT_FILENO);//dup2(3,1);   fd,stdout 其实就是把stdout标准输出的文件指针复制给fd,这样的话,就可以输出到文件了

	execlp("ps", "ps", "ax", NULL);
	//close(fd);这个没用,如果execlp成功执行的话,就不会返回
    //下面的都不会执行了,因为.text   .data都已经替换成ps这个程序的了

	return 0;
}





僵尸进程、孤儿进程

  • 孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程
  • 僵尸进程: 子进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
  • 特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。思考!用什么办法可清除掉僵尸进程呢?用wait、waitpid





wait

一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程

pid_t wait(int *status); 成功:清理掉的子进程ID;失败:-1 (没有子进程)

父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:

  • 阻塞等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)。

当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)

可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:

  • WIFEXITED(status) 为非0 → 进程正常结束 WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
  • WIFSIGNALED(status) 为非0 → 进程异常终止 WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
  • (了解即可) WIFSTOPPED(status) 为非0 → 进程处于暂停状态 WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。 WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>

int main(void)
{
	pid_t pid, wpid;
	int status;

	pid = fork();

	if(pid == -1){
		perror("fork error");
		exit(1);
	} else if(pid == 0){		//son
		printf("I'm process child, pid = %d\n", getpid());
#if 1
		execl("./abnor", "abnor", NULL);
		perror("execl error");
		exit(1);
#endif
		sleep(1);				
		exit(10);
	} else {
		//wpid = wait(NULL);	//传出参数
		wpid = wait(&status);	//传出参数

		if(WIFEXITED(status)){	//正常退出
			printf("I'm parent, The child process "
					"%d exit normally\n", wpid);
			printf("return value:%d\n", WEXITSTATUS(status));

		} else if (WIFSIGNALED(status)) {	//异常退出
			printf("The child process exit abnormally, "
					"killed by signal %d\n", WTERMSIG(status));
										//获取信号编号
		} else {
			printf("other...\n");
		}
	}

	return 0;
}





waitpid

作用同wait,但可指定pid进程清理,可以不阻塞

pid_t waitpid(pid_t pid, int *status, in options); 成功:返回清理掉的子进程ID;失败:-1(无子进程)

特殊参数和返回情况:

参数pid:

  • 大于0 回收指定ID的子进程
  • -1 回收任意子进程(相当于wait)
  • 0 回收和当前调用waitpid一个组的所有子进程
  • < -1 回收指定进程组内的任意子进程

返回:

  • 0:参3为WNOHANG,且子进程正在运行。

注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

用法:

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

int main(void)
{
	pid_t pid, pid2, wpid;
	int flg = 0;

	pid = fork();
	pid2 = fork();

	if(pid == -1){
		perror("fork error");
		exit(1);
	} else if(pid == 0){		//son
		printf("I'm process child, pid = %d\n", getpid());
		sleep(5);				
		exit(4);
	} else {					//parent
		do {
			wpid = waitpid(pid, NULL, WNOHANG);
            //wpid = wait(NULL);
			printf("---wpid = %d--------%d\n", wpid, flg++);
			if(wpid == 0){
				printf("NO child exited\n");
				sleep(1);		
			}
		} while (wpid == 0);		//子进程不可回收

		if(wpid == pid){		//回收了指定子进程
			printf("I'm parent, I catched child process,"
					"pid = %d\n", wpid);
		} else {
			printf("other...\n");
		}
	}

	return 0;
}

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    pid_t pid;
    pid = fork();

    if (pid < 0) {
        perror("fork failed");
        exit(1);
    }

    if (pid == 0) {
        int i;
        for (i = 3; i > 0; i--) {
            printf("This is the child\n");
            sleep(1);
        }
        exit(34);
    } else {
        int stat_val;
        waitpid(pid, &stat_val, 0);     //阻塞

        if (WIFEXITED(stat_val))
            printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
        else if (WIFSIGNALED(stat_val))
            printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
    }
    return 0;
}