Table of Contents
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);
Macro | Description |
---|---|
WIFEXITED(status) | WEXITSTATUS(status) |
WIFSIGNALED(status) | WTERMSIG(status),WCOREDUMP(status) |
WIFSTOPPED(status) | WSTOPSIG(status) |
WIFCONTINUED(status) |
waitpid函数传入pid的含义:
pid | explanation |
---|---|
-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函数实现的)。
近期评论