进程标识
每一个进程都有一个非负的进程ID来唯一标识自己,虽然ID唯一,但是ID可以复用,如果一个进程被销毁了,那么它的ID就可以被新创建的进程所使用。除了进程ID,进程还有其他一些标识符:
uid
:进程的实际用户IDeid
:进程的有效用户IDppid
:进程的父亲进程IDgid
:进程的实际组IDegid
:进程的有效组ID
获得这些标识的接口:
1 | #include <unistd.h> |
uid与euid,gid与egid
这两组值用来管理进程的访问权限。这里只说明uid
和euid
,gid
和egid
作用类似,只不过它们作用于进程组。
uid
指的是登录用户的用户IDeuid
是进程用来决定我们对资源的访问权限,一般实际用户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 | #include <unistd.h> |
设置权限的规则如下:
- 如果进程具有超级用户权限,那么
setuid
讲实际用户ID、有效用户ID、保存的用户ID都设置成uid。 - 如果进程不具有超级用户权限,但是
uid
等于实际用户ID或者保存的用户ID,那么setuid
讲有效用户ID设置成uid
。不更改实际用户ID和保存的用户ID。 - 如果上述规则都不满足,则
errno
设置成EPERM,并返回-1。
另外两个函数:
1 | #include <unistd.h> |
对于一个特权用户来说,seteuid
可以将有效用户ID设置成uid
,而setuid
更改所有的3个用户ID。对于一个非特权用户,可以利用该函数讲有效用户ID设置为实际用户ID或者保存的设置用户ID。
进程创建与销毁
Fork
可以用fork
来创建一个进程,用exit
来销毁一个进程。
1 | #include <unistd.h> |
fork
函数返回两次,在父进程中函数返回值为新生成的子进程的pid
,在子进程中返回值为0。一个比较典型的fork
调用范式为:
1 |
|
fork
通常有一下两种用法:
- 一个父进程希望自己复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中比较常见。
- 一个进程需要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从
fork
中返回后立即调用exec
函数。
fork
的一个特性是,子进程复制父进程所有打开的描述符,这里只是复制文件描述符,父进程和子进程共享文件表项。一个典型的fork
之后的文件描述图示:
除了打开的文件之外,子进程还继承了父进程的:
- 实际用户ID、实际组ID、有效用户ID、有效组ID
- 附属组ID
- 进程组ID
- 会话ID
- 控制终端
- 设置用户ID标识和设置组ID标志
- 当前工作目录
- 根目录
- 文件模式创建屏蔽字
- 信号屏蔽和安排
- 对任一打开文件描述符的执行时关闭
close-on-exec
标志 - 环境
- 连接的共享存储段
- 存储映像
一个fork
的演示例子:
1 | #include <stdio.h> |
输出结果:
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
后主动睡眠,让子进程先执行(实际上这在有的时候也不能保证,需要用更高级的方法),可以看到子进程输出的globalvar
和var
都加了1,而父进程没有。
如果我们重定向了标准输出,那么就会看到before fork
被输出了两次,这是因为当标准输出和一个文件相关联时它是全缓冲的,在fork
之前,字符串被存放到缓冲区中,fork
函数导致父进程的数据空间被复制到子进程中,标准输出的缓冲区也同样被复制,这样当进程结束时,缓冲区被清洗,于是就有了两次brefore fork
的输出。在第一个例子中,由于标准输出和一个终端相关联,它是行缓冲的,所以只输出了一次before fork
。
Exit
进程有5种正常终止以及3种异常终止方式。
正常终止方式:
- 在main函数中调用
return
语句 - 调用
exit
函数,其操作包括调用各个终止函数(由atexit
函数注册),然后关闭所有的标准I/O流。在main
函数里调用return
语句等效于调用exit
函数。 - 调用
_exit
或者_Exit
函数。函数不运行各个终止函数,也不冲洗I/O流,但是它关闭所有打开的文件流。 - 进程的最后一个线程在其启动例程中执行
return
语句,但是该线程的返回值不用作进程的返回值。当最后一个线程中其启动例程返回时,该进程以终止状态0返回。 - 进程的最后一个线程调用pthread_exit函数,进程的终止状态为0。
异常终止方式:
- 调用
abort
函数 - 进程收到信号
- 最后一个线程对”取消”请求作出响应。
进程终止函数由atexit
函数注册:
1 | #include <stdlib.h> |
一个例子:
1 | #include <stdio.h> |
输出:
1 | $ ./a.out At Exit Demo! On exit2 On exit1 On exit1 |
可以看到一个函数可以多次被注册,而且执行的顺序和注册的顺序相反。
在一个进程退出后,如果它有子进程,那么它的所有子进程由init
进程收养。而如果一个子进程在退出后,父进程没有调用wait
或者waitpid
去获取子进程的退出状态,那么子进程就会成为僵尸进程。僵尸进程的地址空间被内核收回,所有打开的文件也被关闭,但是内核保存它的终止状态、进程ID以及CPU时间总量。
wait & waitpid
上一小节说到如果子进程的终止状态没有被父进程通过wait
或者waitpid
得到,那么子进程就会变成僵尸进程。这两个函数的原型如下:
1 | #include <sys/wait.h> |
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 | #include <stdio.h> |
输出:
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 | #include <stdio.h> |
tell_wait
初始化管道wait_xxx
:从管道读端读一个字符,如果写端没有写,那么进程被内核投入睡眠tell_xxx
:从管道写端写,唤醒被睡眠的读端进程吗,,从而达到进程同步的目的。
exec函数
exec
函数族将fork
的程序完全替换为另外一个程序。
1 | #include <unistd.h> |
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 | #include <stdio.h> |
1 | #! /home/ubuntu/Desktop/UNIX-Demos/proc_demos/echoall foo |
echoall程序
1 | #include <stdio.h> |
输出:
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 | #include <unistd.h> |
incr
参数被加到进程当前的nice值上,函数返回进程的nice
值减去NZERO
。所以如果函数返回-1,这时候函数调用可能是出错也可能调用正常,需要检查errno
是否为0。incr
置为0可以得到进程当前的nice
减去NZERO
的值。
系统的NZERO
值可以通过sysconf
函数得到。
进程时间
进程有三个时间:
- 墙上时间:进程运行的总时间
- 用户CPU时间:进程运行用户指令的时间
- 系统CPU时间:程序调用系统调用后,内核执行系统服务所花去的时间。
任何一个进程都可以通过调用times
函数获得它自己以及已终止的子进程的上述三个值。
1 | #include <sys/times.h> |
times
函数返回进程的墙上时钟。注意这里的所有值都是绝对时间,而我们需要的是相对时间,于是就应该在某一个时刻调用times
获得一个初始值,在另外一个时刻调用times
获得一个最终值,其差即为我们所需。
所有由此函数返回的clock_t
值都用_SC_CLK_CTK
(由sysconf
函数返回的每秒时钟滴答数)转换成秒数。
一个例子:
1 | #include <stdio.h> |
输入与输出:
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 |