Zach的博客

进程控制

进程标识

每一个进程都有一个非负的进程ID来唯一标识自己,虽然ID唯一,但是ID可以复用,如果一个进程被销毁了,那么它的ID就可以被新创建的进程所使用。除了进程ID,进程还有其他一些标识符:

  • uid:进程的实际用户ID
  • eid:进程的有效用户ID
  • ppid:进程的父亲进程ID
  • gid:进程的实际组ID
  • egid:进程的有效组ID

获得这些标识的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>

pid_t getpid();

pid_t getppid();

uid_t getuid();

uid_t geteuid();

gid_t getgid();

gid_t getegid();

uid与euid,gid与egid

这两组值用来管理进程的访问权限。这里只说明uideuidgidegid作用类似,只不过它们作用于进程组。

  • uid指的是登录用户的用户ID
  • euid是进程用来决定我们对资源的访问权限,一般实际用户ID等于有效用户ID,当设置用户ID时,有效用户ID等于文件的所有者。

举一个例子,如果一个文件的所有者是root,同时该文件被设置了用户ID,即用ls命令得到了如下的结果

1
$ ls -l system_secure
-rwsrwxr-x 1 root zach41 8720 10月 20 16:07 system_secure

那么当用户zach41执行这个文件时,在执行期间进程获得了文件拥有者root的权限。

我们可以用setuid来设置实际用户ID和有效用户ID。

1
2
3
4
5
#include <unistd.h>

int setuid(uid_t uid);

int setgid(gid_t gid);

设置权限的规则如下:

  1. 如果进程具有超级用户权限,那么setuid讲实际用户ID、有效用户ID、保存的用户ID都设置成uid。
  2. 如果进程不具有超级用户权限,但是uid等于实际用户ID或者保存的用户ID,那么setuid讲有效用户ID设置成uid。不更改实际用户ID和保存的用户ID。
  3. 如果上述规则都不满足,则errno设置成EPERM,并返回-1。

另外两个函数:

1
2
3
4
5
#include <unistd.h>

int seteuid(uid_t uid);

int setegid(gid_t gid);

对于一个特权用户来说,seteuid可以将有效用户ID设置成uid,而setuid更改所有的3个用户ID。对于一个非特权用户,可以利用该函数讲有效用户ID设置为实际用户ID或者保存的设置用户ID。

进程创建与销毁

Fork

可以用fork来创建一个进程,用exit来销毁一个进程。

1
2
3
4
5
6
#include <unistd.h>

pid_t fork();

#include <stdlib.h>
void exit();

fork函数返回两次,在父进程中函数返回值为新生成的子进程的pid,在子进程中返回值为0。一个比较典型的fork调用范式为:

1
2
3
4
5
6
7
8
9

if ((pid = fork) < 0) {
/* fork error*/
...
} else if (pid == 0) {
/* child process*/
} else {
/* parent process*/
}

fork通常有一下两种用法:

  1. 一个父进程希望自己复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中比较常见。
  2. 一个进程需要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork中返回后立即调用exec函数。

fork的一个特性是,子进程复制父进程所有打开的描述符,这里只是复制文件描述符,父进程和子进程共享文件表项。一个典型的fork之后的文件描述图示:

file_shared.png

除了打开的文件之外,子进程还继承了父进程的:

  • 实际用户ID、实际组ID、有效用户ID、有效组ID
  • 附属组ID
  • 进程组ID
  • 会话ID
  • 控制终端
  • 设置用户ID标识和设置组ID标志
  • 当前工作目录
  • 根目录
  • 文件模式创建屏蔽字
  • 信号屏蔽和安排
  • 对任一打开文件描述符的执行时关闭close-on-exec标志
  • 环境
  • 连接的共享存储段
  • 存储映像

一个fork的演示例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <unistd.h>

int globalvar = 6;
char buf[] = "a write to stdout\n";

int main(int argc, char *argv[])
{

int var;
pid_t pid;

var = 88;

if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) {
perror("write error");
return -1;
}
printf("before fork\n");

if ((pid = fork()) < 0) {
perror("fork error");
return -1;
} else if (pid == 0) {
globalvar++;
var++;
} else {
sleep(2);
}

printf("pid = %ld, globalvar = %d, var = %d\n", (long)pid, globalvar, var);

return 0;
}

输出结果:

1
$ ./a.out 
a write to stdout
before fork
pid = 0, globalvar = 7, var = 89
pid = 11510, globalvar = 6, var = 88

$ ./a.out > fork_buf.out

a write to stdout
before fork
pid = 0, globalvar = 7, var = 89
before fork
pid = 11589, globalvar = 6, var = 88

父进程在执行fork后主动睡眠,让子进程先执行(实际上这在有的时候也不能保证,需要用更高级的方法),可以看到子进程输出的globalvarvar都加了1,而父进程没有。

如果我们重定向了标准输出,那么就会看到before fork被输出了两次,这是因为当标准输出和一个文件相关联时它是全缓冲的,在fork之前,字符串被存放到缓冲区中,fork函数导致父进程的数据空间被复制到子进程中,标准输出的缓冲区也同样被复制,这样当进程结束时,缓冲区被清洗,于是就有了两次brefore fork的输出。在第一个例子中,由于标准输出和一个终端相关联,它是行缓冲的,所以只输出了一次before fork

Exit

进程有5种正常终止以及3种异常终止方式。

正常终止方式:

  1. 在main函数中调用return语句
  2. 调用exit函数,其操作包括调用各个终止函数(由atexit函数注册),然后关闭所有的标准I/O流。在main函数里调用return语句等效于调用exit函数。
  3. 调用_exit或者_Exit函数。函数不运行各个终止函数,也不冲洗I/O流,但是它关闭所有打开的文件流。
  4. 进程的最后一个线程在其启动例程中执行return语句,但是该线程的返回值不用作进程的返回值。当最后一个线程中其启动例程返回时,该进程以终止状态0返回。
  5. 进程的最后一个线程调用pthread_exit函数,进程的终止状态为0。

异常终止方式:

  1. 调用abort函数
  2. 进程收到信号
  3. 最后一个线程对”取消”请求作出响应。

进程终止函数由atexit函数注册:

1
2
3
#include <stdlib.h>

int atexit(void (*func)(void));

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>

static void clear_onexit1() {
printf("On exit1\n");
}

static void clear_onexit2() {
printf("On exit2\n");
}

int main(void) {
printf("At Exit Demo!\n");

if (atexit(clear_onexit1) != 0) {
fprintf(stderr, "can't register `clear_onexit1`");
return -1;
}
if (atexit(clear_onexit1) != 0) {
fprintf(stderr, "can't register `clear_onexit1`");
return -1;
}
if (atexit(clear_onexit2) != 0) {
fprintf(stderr, "can't register `clear_onexit1`");
return -1;
}
exit(0);
/* _exit(0); */
}

输出:

1
$ ./a.out 
At Exit Demo!
On exit2
On exit1
On exit1

可以看到一个函数可以多次被注册,而且执行的顺序和注册的顺序相反。

在一个进程退出后,如果它有子进程,那么它的所有子进程由init进程收养。而如果一个子进程在退出后,父进程没有调用wait或者waitpid去获取子进程的退出状态,那么子进程就会成为僵尸进程。僵尸进程的地址空间被内核收回,所有打开的文件也被关闭,但是内核保存它的终止状态、进程ID以及CPU时间总量。

wait & waitpid

上一小节说到如果子进程的终止状态没有被父进程通过wait或者waitpid得到,那么子进程就会变成僵尸进程。这两个函数的原型如下:

1
2
3
4
5
#include <sys/wait.h>

pid_t wait(int *statloc);

pid_t waitpid(pid_t pid, int *statloc, int options);

wait等待任意一个子进程终止,如果没有子进程,那么函数出错。子进程的终止状态被存入statloc指向的内存地址。

waitpid可以等待特定进程终止。options可以进一步控制函数的行为:

  • WCONTINUED:若实现支持作业控制,那么由pid指定的任一子进程在停止后已经继续,但其状态未报告,则返回其状态
  • WNOHANG:若由pid指定的子进程不是立即调用,则waitpid不阻塞,返回值为0。
  • WUNTRACED:若实现支持作业控制,而由pid指定的任一子进程已处于停止状态,并且其状态自停止以来还未报告,则返回其状态。WIFSTOPPED宏确定返回值是否对应于一个停止的子进程。

可以利用以下宏来检查返回的状态值:

  • WIFEXITED(status):若正常终止,返回值为真,此时可执行WEXITSTATUS(status),获取子进程传送给exit或者_exit参数的低8位。
  • WIFSIGNALED(status):若为异常终止子进程,返回真。此时可以执行WTERMSIG(status)获取终止的信号值,而且如果有产生终止进程的core文件,那么WCOREDUMP(status)返回真。
  • WIFSTOPPED(status):若为当前暂停子进程的返回的状态,那么为真。此时可以执行WSTOPSIG(status)得到使得子进程暂停的信号。
  • WIFCONTINUED(status):若在作业控制暂停后已经继续的子进程返回了状态,那么为真。

这里给一个waitpid的例子,同时也说明如何fork两次来让子进程在退出后不会变成僵尸进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

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

if ((pid = fork()) < 0) {
perror("fork error");
return -1;
} else if (pid == 0) {
if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) {
exit(0);
}

sleep(5);
printf("parent process id: %ld\n", (long)getppid());
exit(0);
}

if (waitpid(pid, NULL, 0) != pid) {
perror("wait error");
return -1;
}

return 0;

}

输出:

1
# Zach @ ZachMacbook in ~/Desktop/UNIX_Demos/proc_demos on git:master x [22:26:56] 
$ ./a.out 

# Zach @ ZachMacbook in ~/Desktop/UNIX_Demos/proc_demos on git:master x [22:26:59] 
$ parent process id: 1


# Zach @ ZachMacbook in ~/Desktop/UNIX_Demos/proc_demos on git:master x

可以看到第二个子进程的父进程后来变成了init进程,init进程不断地调用wait来等待其子进程退出,故而第二个子进程不会成为僵尸进程。输出中看到了在./a.out后输出了shell提示符,这是因为父进程首先退出了。

竞争条件

前面提到过,在fork之后我们特意调用sleep函数然父进程睡眠,从而让子进程首先执行,这个方法是欠妥的,在一个非常繁忙的系统中,子进程不一定会先执行。这种不确定性就是进程的竞争条件。为了让子进程首先执行,我们需要一种特殊的手段。这里给出一种利用管道来实现进程同步的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int pipefd[2];

static void tell_wait();
static void tell_parent(pid_t);
static void tell_child(pid_t);
static void wait_child();
static void wati_parent();

static void charatatime(const char*);

int main(void) {
pid_t pid;

tell_wait();

if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid == 0) {
charatatime("output from child\n");
tell_parent(getppid());
} else {
wait_child();
charatatime("output from parent\n");
}
exit(0);
}

static void charatatime(const char* ptr) {
setbuf(stdout, NULL);
for (; *ptr != 0; ptr++) {
sleep(1);
putc(*ptr, stdout);
}
}

static void tell_wait() {
if (pipe(pipefd) < 0) {
perror("pipe error");
exit(-1);
}
}

static void tell_parent(pid_t pid) {
if (write(pipefd[1], "p", 1) != 1) {
perror("write error");
exit(-1);
}
}

static void wait_child() {
char c;
int n;
if ((n = read(pipefd[0], &c, 1)) < 0) {
perror("read error");
exit(-1);
}
if (c != 'p') {
fprintf(stderr, "wait child error");
exit(-1);
}
}
  • tell_wait初始化管道
  • wait_xxx:从管道读端读一个字符,如果写端没有写,那么进程被内核投入睡眠
  • tell_xxx:从管道写端写,唤醒被睡眠的读端进程吗,,从而达到进程同步的目的。

exec函数

exec函数族将fork的程序完全替换为另外一个程序。

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>

int execl(const char* pathname, const char *arg0, ... /* (char *)0*/);
int execv(const char *pathname, char *const argv[]);
int ececle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[]*/);
int execve(const char *pathname, char *const agrv[], 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[]);

exec函数族命名规则如下:

  • 含有l:程序参数以列表形式arg0, arg1, ... argn, (char *)0形式传入。
  • 含有v:程序参数以向量形式argv[]传入,数组最后一个元素为NULL。
  • 含有p:程序文件的路径名在环境变量PATH指定的路径下寻找
  • 含有e:程序的环境变量由参数envp提供,否则继承父进程的环境变量
  • 含有f:由文件描述符来代表程序文件。

解释器文件

解释器文件是一种文本文件,它的起始行格式为:

1
#! pathname [optional-arguement]

pathname一般为绝对路径。对这种文件的识别是由内核作为exec系统调用处理的一部分来完成的。内核使得调用exec函数的进程实际执行的不是这个解释器文件,而是在解释器文件第一行pathname所指定的文件,即解释器为pathname指定的文件,解释器文件为该文件。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{

pid_t pid;

if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid == 0) {
if (execl("./testinterp", "testinterp", "myarg1", "MY ARG2", (char *)0) < 0) {
perror("execl error");
exit(-1);
}
}
if (waitpid(pid, NULL, 0) < 0) {
perror("waitpid error");
exit(-1);
}
return 0;
}
1
#! /home/ubuntu/Desktop/UNIX-Demos/proc_demos/echoall foo

echoall程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>

extern char **environ;

int main(int argc, char *argv[])
{

int i;
char **ptr;

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

/* for (ptr = environ; *ptr != 0; ptr++) { */
/* printf("%s\n", *ptr); */
/* } */
return 0;
}

输出:

1
argv[0] : /home/ubuntu/UNIX-Demos/proc_demos/echoall
argv[1] : foo
argv[2] : ./testinterp
argv[3] : myarg1
argv[4] : MY ARG2
ubuntu@VM-121-120-ubuntu:~/UNIX-Demos/proc_demos$

如一个awk脚本文件为:

1
#!/usr/bin/awk -f
# A awk example
BEGIN {
      for (i=0; i<ARGC; i++)
          printf "ARGV[%d] = %s\n", i, ARGV[i]
      exit	  
}

当我们执行./awkeample file1 file2时,命令以以下方式被执行:

1
/bin/awk -f /path/to/awkexample file1 file2

于是awk程序执行该脚本输出所有参数的值。

进程调度

可以通过nice值来调控进程优先级,进程的nice值范围在[0, 2*NZERO-1],nice值高的进程优先级越低(表示越友好)。进程可以通过nice函数接口来获得或者设置nice值,进程只能设置自己的nice值,而无法影响其他进程的nice值。

1
2
3
#include <unistd.h>

int nice(int incr);

incr参数被加到进程当前的nice值上,函数返回进程的nice值减去NZERO。所以如果函数返回-1,这时候函数调用可能是出错也可能调用正常,需要检查errno是否为0。incr置为0可以得到进程当前的nice减去NZERO的值。

系统的NZERO值可以通过sysconf函数得到。

进程时间

进程有三个时间:

  • 墙上时间:进程运行的总时间
  • 用户CPU时间:进程运行用户指令的时间
  • 系统CPU时间:程序调用系统调用后,内核执行系统服务所花去的时间。

任何一个进程都可以通过调用times函数获得它自己以及已终止的子进程的上述三个值。

1
2
3
4
5
6
7
8
9
10
#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 childrem
}

times函数返回进程的墙上时钟。注意这里的所有值都是绝对时间,而我们需要的是相对时间,于是就应该在某一个时刻调用times获得一个初始值,在另外一个时刻调用times获得一个最终值,其差即为我们所需。

所有由此函数返回的clock_t值都用_SC_CLK_CTK(由sysconf函数返回的每秒时钟滴答数)转换成秒数。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <stdio.h>
#include <sys/times.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

void pr_exit(int);
void pr_times(clock_t, struct tms *, struct tms *);
void do_cmd(char *);

int main(int argc, char *argv[])
{

setbuf(stdout, NULL);
for (int i=1; i<argc; i++)
do_cmd(argv[i]);
return 0;
}

void pr_exit(int status) {
if (WIFEXITED(status)) {
printf("normal termination, exit status = %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("abnormal termination, signal number = %d%s\n", WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? "(core file generated)" : "");
#else
"");
#endif
} else if (WIFSTOPPED(status)){
printf("child stopped, signal number = %d\n", WSTOPSIG(status));
}
}

/*
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_sutime; // system CPU time, terminated children
}
*/

void pr_times(clock_t real, struct tms *tmsstart, struct tms *tmsend) {
static long clktck = 0;
if (clktck == 0) {
if ((clktck = sysconf(_SC_CLK_TCK)) < 0) {
perror("sysconf error");
exit(-1);
}
}

printf(" real: %7.2f\n", real / (double)clktck);
printf(" user: %7.2f\n", (tmsend -> tms_utime - tmsstart -> tms_utime) / (double)clktck);
printf(" sys: %7.2f\n", (tmsend -> tms_stime - tmsstart -> tms_stime) / (double)clktck);
printf(" child user: %7.2f\n", (tmsend -> tms_cutime - tmsstart -> tms_cutime) / (double)clktck);
printf(" child sys: %7.2f\n", (tmsend -> tms_cstime - tmsstart -> tms_cstime) / (double)clktck);
}

void do_cmd(char *cmd) {
struct tms tmsstart, tmsend;
clock_t start, end;
int status;

printf("\nCommand: %s\n", cmd);

if ((start = times(&tmsstart)) == -1) {
perror("times error");
exit(-1);
}

if ((status = system(cmd)) < 0) {
perror("system() error");
exit(-1);
}

if ((end = times(&tmsend)) == -1) {
perror("times error");
exit(-1);
}
pr_times(end - start, &tmsstart, &tmsend);
pr_exit(status);
}

输入与输出:

1
$ ./a.out "sleep 5" "date" "man bash > /dev/null"

Command: sleep 5
  real:     5.02
  user:     0.00
  sys:     0.00
  child user:     0.00
  child sys:     0.00
normal termination, exit status = 0

Command: date
Thu Oct 20 23:58:06 CST 2016
  real:     0.02
  user:     0.00
  sys:     0.00
  child user:     0.00
  child sys:     0.00
normal termination, exit status = 0

Command: man bash > /dev/null
  real:     0.24
  user:     0.00
  sys:     0.00
  child user:     0.28
  child sys:     0.07
normal termination, exit status = 0