计算机 / 读书笔记 · 2021年12月19日 0

APIUE读书笔记chapter8

Process Identifiers

pid,非负整数。unique,但是进程结束后可以被回收重利用。pid为0的通常是调度进程(scheduler process),通常称之为swapper进程。swapper是系统进程,是kernel的一部分。pid为1的是init进程,就是bootstrap结束时启动的进程。init进程是用户进程而非系统进程,但是init进程照样拥有超级用户权限。

#include <unistd.h>

pid_t getpid(void);

pid_t getppid(void);

uid_t getuid(void);

uid_t geteuid(void);

gid_t getgid(void);

gid_t getegid(void);

fork函数

#include <unistd.h>

pid_t fork(void);

注意write是非buffered,而标准I/O库在输出到terminal时是line buffered,在输出到文件时则buffer得更多(fully buffered),可能在process终止,做cleanup工作时才把buffered的内容flush到文件里。所以在fork时,父进程在标准I/O库里缓存的buffer也会拷贝一份给子进程。

#include "apue.h"
int
char
globvar = 6;
/* external variable in initialized data */
buf[] = "a write to stdout\n";
int
main(void)
{
	int
		pid_t
		var;
	pid;
	/* automatic variable on the stack */
	var = 88;
	if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
		err_sys("write error");
	printf("before fork\n");
	/* we don’t flush stdout */
	if ((pid = fork()) < 0) {
		err_sys("fork error");
	} else if (pid == 0) {
		globvar++;
		var++;
	} else {
		sleep(2);
	}
	/* child */
	/* modify variables */
	/* parent */
	printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), globvar,
			var);
	exit(0);
}

书上举的上面这个例程在./a.out./a.out > temp.out这两种运行方式下输出的内容是不同的。

文件共享

fork时,父进程打开的文件描述符会都拷贝一份给子进程,父进程和子进程通过fork拷贝生成的每个文件描述符会共享同一个file table entry。如下面示意图:

fork时,父进程和子进程不会共享(不相同)的东西:

  • fork的返回值
  • pid
  • ppid
  • 子进程的tms_utime,tms_stime,tms_cutime,tms_cstime等值被设为0
  • 父进程的文件锁(file locks)不被子进程继承
  • 子进程的pending alarms被清除
  • 子进程的pending signals被清除

fork两种常见用途:

  • 网络应用fork子程序作为worker
  • shell创建新的进程执行相应的命令(fork后接着执行exec函数)

vfork

不要庸人自扰使用vfork。vfork只是由于历史原因而存在过,已经从Unix Specification Version 4中完全移除。
vfork与fork的区别:vfork创建的子进程不会创建一份父进程地址空间的拷贝,而是直接在父进程的地址空间中运行,直到调用exit或exec函数后,子进程才会退出或在自己的地址空间中运行相应程序,父进程才会结束vfork继续运行。vfork存在的意义就在于子进程没有像fork那样拷贝父进程的地址空间,带来了某种意义上的性能提升。

exit functions

孤儿进程:父进程先于子进程结束,就将子进程过继给init进程,由init进程负责子进程的善后;
僵尸进程:子进程已经结束,父进程还没有通过wait、waitpid等函数获取子进程退出状态,导致子进程的尸体还被系统保留着(pid,退出状态值,使用的cpu时间等)。

wait与waitpid

#include <sys/wait.h>

pid_t wait(int *statloc);

pid_t waitpid(pid_t pid, int *statloc, int options);
MacroDescription
WIFEXITED(status)WEXITSTATUS(status)
WIFSIGNALED(status)WTERMSIG(status),WCOREDUMP(status)
WIFSTOPPED(status)WSTOPSIG(status)
WIFCONTINUED(status)

waitpid函数传入pid的含义:

pidexplanation
-1等待任何一个子进程
正整数等待具有此pid的子进程
0等待任何与此进程具有相同相同进程组ID的子进程
负数等待任何进程组id与此pid绝对值相等的子进程

waitpid可以传入的options:

|WCONTINUED |如果系统支持job control,那么就返回相应的被stop然后又continue,且stop后其进程状态没有汇报过的进程的退出状态 | |WNOHANG |不要阻塞式地等待对应子进程,子进程没有结束就立即返回,不要干等着 | |WUNTRACED |如果系统支持job control,那么就返回被stop,且stop后其进程状态没有汇报过的进程的退出状态 |

waitid函数

#include <sys/wait.h>

int waitid(idtype_t idtype, id_t id, siginfo_t *info, int options);

idtype可取值:P_PID,P_PGID,P_ALL。
options可取值:至少包含WCONTINUED,WEXITED,WSTOPPED中的一个,WNOHANG,WNOWAIT可选。
貌似BSD不支持这个函数。

wait3和wait4函数

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

wait3和wait4提供了的,但是wait,waitpid,waitid没有提供的东西是,wait3和wait4可以返回退出进程的资源使用情况。

Race Conditions

apiue介绍了5种用于进程间同步的方法。

exec函数族

#include <unistd.h>

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[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);

名字中带p的表示会通过环境变量PATH寻找相应的可执行程序,带l的表示会传入参数列表,带v的表示传入的参数列表是通过指向参数列表的指针传入的,带e的表示要传入环境变量列表指针,带f的只有一个,表示直接传入可执行程序文件的文件描述符,防止恶意程序在系统定位到pathname或者filename表示的可执行程序文件之后修改可执行程序文件的内容,或者另外一种情况:系统先检测找到的可执行程序文件是否是可执行程序文件,不是就拿给/bin/sh当做脚本执行,而fexecve的作用是告诉系统一定要把这个文件当做可执行程序加载(不要觉得不是而当做脚本去运行)。
执行exec函数族之前打开的文件描述符在执行exec函数时是否需要关闭取决于当初创建文件描述符时设置的close-on-exec(FD_CLOEXEC)标志位。如果当初设置了这个标志位,进程执行exec函数族时就关闭这个文件描述符,否则就仍然让其处于打开状态。也可以在exec函数之前通过fcntl函数手动修改文件描述符的FD_CLOEXEC标志位,让进程在执行exec函数时关闭掉/或不关该文件描述符。
打开的directory stream在执行exec函数时则会被关闭,因为opendir函数有调用fcntl设置close-on-exec标志位。
调用exec后,real user ID和real groud ID仍然不变,但是effective user/group ID可能会变。这取决于运行的程序文件中的set-user-ID位和set-group-ID位。如果运行的程序文件有设置这些位,那么effective user/group ID就变为该程序文件的owner ID。
这七个函数一般只有一个是kernel的系统调用,其他几个函数最终都靠调用这个系统调用实现功能,比如下面是一种实现方式:

exec函数传入的参数列表的第一个(第0个)参数一般设置为要执行的程序的名字(可能还带了路径),这大概就是main函数里传入的参数列表的第一个值为main函数所在程序名字的原因。

更改user id和grop id

#include <unistd.h>

int setuid(uid_t uid);

int setgid(gid_t gid);

#include <unistd.h>

int seteuid(uid_t uid);

int setegid(gid_t gid);

因为跳过了前面的章节,没搞清楚real-user-id,effective-user-id,saved userd id的含义和区别。。。

User Identification

#include <unistd.h>

char *getlogin(void);

Interpreter Files

#! pathname [ optional-argument ]中感叹号后面的空格可省略。pathname一般需要是绝对路径,因为这里不适用PATH环境变量。

Interpterter script和bash script的区别,看了半天,没看明白。

system函数

#include <stdlib.h>

int system(const char *cmdstring);

system函数其实也是通过fork、exec和waitpid实现的(在某个shell中执行命令)。

Set-User-ID programs

不要在set-user-ID、set-group-ID程序中执行system函数,因为set-user-ID、set-group-ID程序的特殊权限会被system中生成的子进程继承, it’s dangerous。

Process Accounting

进程相关的统计,暂时不想看这部分的东西。

Process Scheduling

#include <unistd.h>

int nice(int incr);

#include <sys/resource.h>

int getpriority(int which, id_t who);

int setpriority(int which, id_t who, int value);

Process Times

#include <sys/times.h>

clock_t times(struct tms *buf);

struct tms {
	clock_t tms_utime;	/* user CPU time */
	clock_t tms_stime;	/* system CPU time */
	clock_t tms_cutime;	/* user CPU time, terminated children */
	clock_t tms_cstime;	/* system CPU time, terminated children */
};

wall clock时间是以times函数返回值的形式返回的。对于这个返回的wall clock我们只能通过以作差的方式来使用,而不能直接将其转换为与现实时间对应的时刻。
作差得到的wall clock时间和tms结构替中的时间都需要除以通过sysconf函数查询_SC_CLK_TCK的相应结果才能转换为秒数。

tms结构体中的tms_cutime和tms_cstime只包含了父进程通过wait一类函数等待过的子进程的user cpu time和system cpu time。

所以我们可以通过这个times函数实现shell中常用的time命令(根据time命令的manual,time命令显示的更多信息是用wait3函数实现的)。

Exercises