【UNIX/Linux】进程控制

本文是笔者拜读《UNIX环境高级编程》第8章(进程控制)的学习笔记。本文的主要内容包括进程标识、fork、exit、wait、竞争条件、exec、解释器文件、system、本章习题。

进程标识

每个进程都有一个非负整型表示的唯一进程ID

系统中有一些专用进程。ID为0的进程通常是调度进程(交换进程,系统进程),是内核的一部分。
ID为1的通常是init进程,此进程负责在自举内核后启动一个UNIX系统。init进程不会终止,它是一个普通的用户进程,但它以超级用户特权运行。
某些系统的进程ID 2是页守护进程,此进程负责支持虚拟存储器系统的分页操作。

除了进程ID,每个进程还有一些其它的标识符,下面的函数返回这些标识符:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这些函数都没有出错返回。

函数fork

一个进程可以调用fork创建一个新进程。
在这里插入图片描述
子进程返回0,父进程返回子进程的ID,出错返回-1
父进程和子进程共享正文段,但各自拥有独立的数据副本。
写时复制:父子进程共享数据,当其中一个试图修改这些区域,内核只为修改区域的那块内存制作一个副本。

如果未刷新缓冲区,子进程也会继承父进程的缓冲区。sizeof运算是在编译时计算的。

调用了fork的进程,一种是希望复制自己,一种是希望执行别的进程。

函数vfork

在这里插入图片描述
vfork也用于创建一个新进程,而该新进程的目的是exec一个新程序,如shell

vfork不将父进程的地址空间完全复制到子进程,在子进程调用execexit之前,它在父进程的空间中运行(共享代码和数据)。如果子进程修改了数据,则会带来未知的结果。

vfork保证子进程先运行,在子进程调用execexit之后,父进程才能被调度运行。

_exit不执行终止处理程序和标准I/O缓冲区的冲洗操作。

在大多数exit的实现中不会关闭流,因为进程终止时,内核会自动关闭进程中的所有文件描述符。在库中关闭这些,增加开销而且可能带来麻烦。

函数exit

exit调用_exit_exit_Exit是同义的。不管进程如何终止,最后都会执行内核中的同一段代码,这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

子进程将自己的退出状态作为参数传递给exit系列函数,父进程通过waitwaitpid函数获取其终止状态。在最后调用_exit时,内核将退出状态转换成终止状态。

对于父进程已经终止的进程,它们的父进程都改变为init进程,即init进程收养。

一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程。

函数wait和waitpid

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD异步信号。默认情况下,父进程忽略该信号。

调用waitwaitpid时:
(1)如果其子进程还在运行,则阻塞。
(2)如果一个子进程终止,正等待父进程获取其终止状态,则取得该子进程的终止状态并返回。
(3)如果它没有任何子进程,则立即出错返回。

如果进程在接收到SIGCHLD信号后调用wait,则立即返回。

在这里插入图片描述
在一个子进程终止前,wait使其调用者阻塞,而waitpid可使调用者不阻塞。
wait等待的是第一个终止子进程,waitpid可以等待指定的子进程。

参数status是子进程终止状态的地址,如果该值为空,则父进程获取不到终止状态。

在这里插入图片描述
进程异常退出时,可能产生终止进程的core文件。

对于waitpid中的pid参数:
(1)pid == -1 等待任一子进程,和wait等效。
(2)pid > 0 等待进程IDpid相等的子进程。
(3)pid == 0 等待组ID等于调用进程组ID的任一子进程。
(4)pid < -1 等待组ID等于pid绝对值的任一子进程。

options参数使我们能进一步控制waitpid的操作,可以提供非阻塞版本。

函数waitid

waitid类似于waitpid但提供了更多的灵活性,
在这里插入图片描述
waitid允许一个进程指定要等待的子进程,但它使用两个单独的参数表示要等待的子进程所属的类型。
在这里插入图片描述
options参数是下列各标志的位或运算。
在这里插入图片描述
infop参数是指向siginfo_t结构的指针,该结构包含了造成子进程状态改变有关信号的详细信息。

函数wait3和wait4

wait3wait4允许内核返回由终止进程及其所有子进程使用的资源概况。
在这里插入图片描述
资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。

竞争条件

当多个进程都企图对共享数据进行某种处理,而最后的结果取决于进程运行的顺序时,我们认为发生了竞争条件

如果一个进程希望等待一个子进程终止,则它必须调用wait函数中的一个。如果一个进程要等待其父进程终止,则可以使用循环(等待自己被init进程收养):

while (getppid() != -1) {
	sleep(1);
}

为避免竞争条件和轮询(白白浪费CPU资源),使用睡眠锁实现互斥与同步地访问临界资源。

函数exec

当进程调用一种exec函数时,该进程执行的程序完全被替换为新程序,而新程序则从其main函数开始执行。exec是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆和栈段。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

如果file中包含/,则将其视为路径名。否则就按PATH环境变量,在它所指定的目录中搜寻可执行文件。

PATH变量包含一张目录表(路径前缀),目录之间用:分隔。如:
在这里插入图片描述
最后的.表示当前目录,空路径前缀也表示当前目录。
如果exec使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则认为该文件是一个shell脚本,于是试着调用/bin/sh,并将file作为shell的输入。

调用exec函数时,通过路径名、文件名、文件描述符指定可执行文件,指针数组、多个字符串地址(包含空指针)指定参数,还可以指定环境表。

在调用了exec函数的新程序中,对打开文件的处理与每个描述符的执行时关闭(close_on_exec)标志FD_CLOEXEC有关。进程中每个打开描述符都有一个执行时关闭标志,若设置了该标志,则在执行exec时关闭该描述符(使用fcntl设置该标志),否则该描述符仍然打开(默认)。

exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变取决于所执行程序文件的设置用户ID位和设置组ID位是否设置。

只有execve是系统调用,其它都是库函数。
在这里插入图片描述

更改用户ID和更改组ID

UNIX系统中,特权和访问控制是基于用户ID和组ID的,要改变特权就需要更改ID。可以使用setuid设置实际用户ID和有效用户ID,用setgid设置实际组ID和有效组ID
在这里插入图片描述
在这里插入图片描述
更改用户ID的规则:
(1)若进程具有超级用户权限,则setuid将实际用户ID、有效用户ID以及保存的设置用户ID设置位uid
在这里插入图片描述
(2)若进程没有超级用户权限,但是uid等于实际用户ID或保存的设置用户ID,则setuid只将有效用户ID设置为uid,不更改实际用户ID和保存的设置用户ID。
(3)以上条件都不满足的话,将errno设置为EPERM,返回-1.

关于内核所维护的3个用户ID,要注意以下几点:
(1)只有超级用户进程可以更改实际用户ID
(2)仅当对程序文件设置了设置用户ID位时,exec函数才设置有效用户ID
(3)保存的设置用户ID是由exec复制有效用户ID得到的。
在这里插入图片描述

函数setreuid和setregid

交换实际用户和有效用户的ID值。
在这里插入图片描述
如若其中任一参数的值为-1,则相应的ID保持不变。
一个非特权用户总能交换实际用户ID和有效用户ID

函数seteuid和setegid

只更改有效用户ID和有效组ID
在这里插入图片描述
一个非特权用户可将其有效用户ID设置为实际用户ID或保存的设置用户ID。特权用户可将有效用户ID设置为euid
在这里插入图片描述
这些修改ID的类似方式适用于各个组ID,附属组不受setgidsetregidsetegid的影响。

解释器文件

解释器文件是一种文本文件,起始行的形式是:#! pathname[optional-argument],如#! /bin/shpathname是解释器,进程实际执行的是解释器。

函数system

在这里插入图片描述
system在其实现中调用了forkexecwaitpid,有三种返回值:
(1)fork失败或者waitpid返回除EINTR之外的错误,则返回-1.
(2)如果exec失败,则返回值如同shell执行了exit(127)
(3)返回shell的终止状态。
在这里插入图片描述
shell-c选项告诉shell程序取下一个命令行参数(cmdstring)作为命令输入。

进程会计

大多数UNIX系统提供了一个选项以进行进程会计处理。启用该选项后,每当进程结束时内核就写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名、CPU时间总量、用户ID、组ID、启动时间等。

用户标识

我们可以调用getpwuid(getuid())找到运行该进程的用户登录名,如果一个用户有多个登录名(一个人在口令文件中可以有多个登录项,它们的用户ID相同,但登录shell不同),用getlogin获取此登录名。
在这里插入图片描述
如果调用此函数的进程没有连接到用户登录时所用的终端,则函数会失败,通常称这些进程为守护进程

进程调度

调度策略和调度优先级是由内核确定的,只有特权进程允许提高调度权限。
nice值越小,优先级越高。

nice意为“友好的”,越不友好的进程,优先级越高(参考排队和插队)

进程通过nice函数获取或更改它的nice值,使用这个函数,进程只能影响自己的nice值。
在这里插入图片描述
getpriority函数可以像nice函数那样获取nice值,但它还可以获取一组相关进程的nice值。
setpriority函数可用于为进程、进程组和属于特定用户ID的所有进程设置优先级。
在这里插入图片描述
Linux中,子进程从父进程中继承nice值。

进程时间

进程的时间包括:墙上时钟时间、用户CPU时间和系统CPU时间。任一进程都可调用times函数获得它自己以及已终止子进程的上述值。
在这里插入图片描述
在这里插入图片描述
若成功,函数返回墙上时钟时间(单位是时钟滴答数),出错返回-1

习题

8.1

题目: 在下面的程序中,如果用exit替换_exit调用,那么可能会使标准输出关闭,使printf返回-1。修改程序以验证在你所使用的系统上是否会产生此种结果。如果并非如此,你怎样处理才能得到类似的结果?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
答: 手动关闭标准输出缓冲区。

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

int global = 6;

int main() {
	int var = 88;
	printf("before vfork\n");
	
	pid_t pid = vfork();
	if (pid < 0) {
		perror("vfork error");
	}
	else if (pid == 0) {
		++global;
		++var;
		exit(0);
	}
	
	close(STDOUT_FILENO);
	if (printf("%d %d\n", global, var) < 0) {
		perror("printf error");
	}
	
	return 0;
}

运算结果:
在这里插入图片描述
vfork:父子进程共享数据(包括缓冲区)。
fork:子进程获得父进程的副本(写时复制)。
exit_exit最大的区别在于:exit会刷新缓存区、执行终止处理程序,而_exit直接关闭文件描述符、清理资源。

8.2

题目: 由于对应于每个函数调用的栈帧通常存储在栈中,并且由于调用vfork后,子进程运行在父进程的地址空间中,如果不是在main函数中而是在另一个函数中调用vfork,此后子进程又从该函数返回,将发生什么?
答: 使用vfork创建的子进程在调用execexit之前,和父进程共享数据(包括栈帧),且阻塞着父进程。如果子进程从函数返回,则该函数对应的栈帧被释放掉,父进程无法访问该栈帧里的数据。

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

pid_t f(int n) {
	pid_t t = vfork();
	if (t < 0) {
		perror("vfork error");
	}
	else if (t == 0) {
		printf("child...\n");
		printf("%d\n", n);
	}
	else {
		printf("parent in  f...\n");
		printf("%d\n", n);
	}
	return t;
}

int main() {
	printf("before f\n");
	pid_t t = f(55);
	printf("after f\n");
	if (t == 0) {
		exit(0);
	}
	
	return 0;
}

运行结果:
在这里插入图片描述
子进程先执行直到退出,父进程在访问函数f里的自动变量时出现段错误。

8.3

题目: 重写下列程序,把wait换成waitid,不调用pr_exit,而是从siginfo结构中确定等价的信息。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
答:

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

void prtExit(siginfo_t *info) {
	printf("status: %d\n", info->si_status);
	switch (info->si_code) {
	case (CLD_EXITED):
		printf("正常退出\n");
		break;
	case (CLD_KILLED):
		printf("被信号杀死\n");
		break;
	case (CLD_DUMPED):
		printf("异常退出,核心转储\n");
		break;
	case (CLD_STOPPED):
		printf("由信号停止\n");
		break;
	default:
		printf("其他情况");
		break; 
	}
}

int main() {
	pid_t t;
	siginfo_t info;

	if ((t = fork()) < 0) {
		perror("fork error");
	}
	else if (t == 0) {
		exit(7);
	}
	waitid(P_PID, t, &info, WEXITED);
	prtExit(&info);	
	
	if ((t = fork()) < 0) {
		perror("fork error");
	}
	else if (t == 0) {
		abort();
	}
	waitid(P_PID, t, &info, WEXITED);
	prtExit(&info);

	if ((t = fork()) < 0) {
		perror("fork error");
	}
	else if (t == 0) {
		int n = 1 / 0;
	}
	waitid(P_PID, t, &info, WEXITED);
	prtExit(&info);
	
	return 0;
}

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

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,即core dump(核心转储),该文件可用于后续调试和检查错误。

8.4

**题目:**执行下面的程序一次,其输出是正确的,但是若将该程序执行多次,则输出不正确。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
原因是什么?怎样才能更正此类错误?如果使子进程首先输出,还会发生此问题吗?
答: 即使子进程首先输出,也不会有改善。上图的指令表示,shell创建了3个子进程,且并发地执行它们,这3个子进程之间并没有设置互斥或同步关系,所以输出的数据比较混乱。

每个进程和它的子进程之间是互斥同步的。如果这三个进程是依次执行而不是并发执行,那笔者也不知道答案。。。

8.5

题目: 在下面的程序中,调用execl,指定pathname为解释器文件。如果将其更改为调用execlp,指定testinterpfilename,并且如果目录/home/sar/bin是路径前缀,则运行该程序时,argv[2]的打印输出是什么?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
答:

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

int main() {
	putenv("PATH=.");
	pid_t t;
	if ((t = fork()) < 0) {
		perror("fork error");
	}
	else if (t == 0) {
		if (execlp("test", "test2", "arg1", "arg2", (char*)0) < 0) {
			perror("exec error");
		}	
	}
	printf("finish\n");
	waitpid(t, NULL, 0);	
	return 0;
}
// test
#! /home/liheng/UNIX_learning/8/test/echoArg testArg1 testArg2
// echoArg.c
#include <stdio.h>

int main(int argc, char **argv) {
	printf("%d arguments\n", argc);
	for (int i = 0; i < argc; ++i) {
		printf("argv[%d]: %s\n", i, argv[i]);
	}
	return 0;
}

运行结果:
在这里插入图片描述
argv[2]打印输出的是./testinterp

8.6

题目: 编写一段程序创建一个僵死进程,然后调用system执行ps命令以验证该进程是僵死进程。
答:
僵死进程:子进程已退出,而父进程既没退出(没法被init收养)也没调用wait清理子进程资源,此时子进程是僵死进程。

// t6.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main() {
	pid_t t;
	
	if ((t = fork()) < 0) {
		perror("fork error");
	}
	else if (t == 0) {
		printf("child ID: %d\n", getpid());
		exit(0);
	}
	else {
		printf("parent ID: %d\n", getpid());
		sleep(2);
		char str[100];
		// 输入search查看僵死进程
		while (fgets(str, 100, stdin)) {
			if (strcmp(str, "search\n") == 0) {
				break;
			}
		}
		sprintf(str, "ps -el | grep %d", t);
		if (system(str) < 0) {
			perror("system error");
		} 
		// 输入kill杀死僵死进程及其父进程
		while (fgets(str, 100, stdin)) {
			if (strcmp(str, "kill\n") == 0) {
				break;
			}
		}
		exit(0);
	}
}

父进程退出时,如果子进程已经退出了,就会自动回收子进程。

运行结果:
在这里插入图片描述
Z表示僵死(zombie)进程。

8.7

题目: POSIX要求在exec时关闭打开目录流。按下列方法对此进行验证:对根目录调用opendir,查看在你的系统上实现的DIR结构,然后打印执行时关闭标志。接着打开同一目录读并打印执行时关闭标志。
答:

// t7.c
#include <stdio.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
	char path[] = "/";
	DIR *dir = opendir(path);
	int fd = dirfd(dir);

	int flag = fcntl(fd, F_GETFD);
	printf("%s close-on-exec: %d\n", path, flag);
	
	struct dirent *d;
	chdir(path);
	while (d = readdir(dir)) {
		char str[100];
		sprintf(str, "%s%s", path, d->d_name);
		fd = open(d->d_name, O_RDONLY);
		if (fd < 0) {
			fprintf(stderr, "%s ", str);
			perror("open error");
			continue;
		}
		flag = fcntl(fd, F_GETFD);
		printf("%s close-on-exec: %d\n", str, flag);
	}
	
	return 0;
}

root用户下启动进程,否则打不开某些文件:
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值