进程简介
本文简单说明了linux操作系统中进程的概念,以及进程相关的简单操作。
进程基础
程序的执行实例被称为进程(process),Unix系统确保每个进程都有唯一的数字标识符,称为进程ID(process ID)。进程ID是一个非负的整数。可以使用getpid()
函数来获得进程ID。
#include <unistd.h>
#include <stdio.h>
int main(void)
{
printf("process ID %d\n", getpid());
return (0);
}
运行程序:
$ ./process
process ID 39572
或者使用ps
命令,获得所有进程信息:
PID TTY TIME CMD
15252 ttys002 0:00.25 ssh -p 10022 zhf@127.0.0.1
35084 ttys003 0:00.33 vim lab05_extra.py
36715 ttys003 0:00.05 python
进程控制
有2个用于进程控制的主要函数:fork
和waitpid
。我们用一个小程序来说明他们的具体作用:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
int status;
if ((pid = fork()) < 0) {
perror("fork error!\n");
exit(EXIT_FAILURE);
}
/* children process */
if (pid == 0) {
printf("children process %d.\n", getpid());
}
/* parent process */
else {
printf("parent process %d.\n", getpid());
waitpid(pid, &status, 0);
printf("%d process stop!\n", pid);
}
return 0;
}
我们运行程序,得到以下结果:
parent process 40050.
children process 40051.
40051 process stop!
fork()
会创建一个新的进程,新进程是原始进程的副本,和原始进程有相同的环境变量,我们称新创建的进程为子进程。fork()
对父进程返回子进程的ID,对子进程返回0。因为fork()
创建了一个新的进程,所以说它被调用一次,执行两次。waitpid()
接收进程ID,等待进程结束,并返回子进程的终止状态(保存在status
中)。
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用waitpid
获取这些信息,然后彻底清除掉这个进程。
如果一个进程已经终止,但是它的父进程尚未调用waitpid
对它进行清理,这时的进程状态称为僵尸(Zombie)进程。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了,为了观察到僵尸进程,我们自己写一个不正常的程序,父进程fork
出子进程,子进程终止,而父进程既不终止也不调用waitpid
清理子进程:
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
pid_t pid=fork();
if(pid<0) {
perror("fork");
exit(1);
}
/* parent process */
if (pid>0) {
while(1);
}
/* child */
return 0;
}
在后台运行这个程序,然后用ps命令查看:
$ ./err
[1] 40457
$ ps u
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
hanfeng 40457 100.0 0.0 4277984 852 s001 RN 8:59PM 0:29.34 ./err
hanfeng 40461 0.0 0.0 0 0 s001 ZN 8:59PM 0:00.00 (err)
...
父进程的pid是40457,子进程是僵尸进程,pid是40461,ps
命令显示僵尸进程的状态为ZN。
如果一个父进程终止,而它的子进程还存在(这些子进程或者仍在运行,或者已经是僵尸进程了),则这些子进程的父进程改为init进程。init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止,init就会调用wait函数清理它。
环境变量
每个程序都有一张环境表,环境表是一个字符串指针数组。全局变量environ
包含了该指针数组的地址。
由于我们的bash同样是一个进程,我们可以在terminal中使用env
来查看所有的环境变量:
...
USER=hanfeng
XPC_SERVICE_NAME=0
LOGNAME=hanfeng
__CF_USER_TEXT_ENCODING=0x0:0:0
ITERM_SESSION_ID=w1t0p0:416DCE2A-C001-4CA9-B966-3075F6A45EC8
SHLVL=1
OLDPWD=/Users/hanfeng/Desktop
ZSH=/Users/hanfeng/.oh-my-zsh
PAGER=less
LESS=-R
LSCOLORS=Gxfxcxdxbxegedabagacad
LC_CTYPE=UTF-8
_=/usr/bin/env
使用fork()
产生的子进程都会复制父进程的环境变量。因此如果使用export
改变环境变量,那么子进程的环境变量也会相应改变。如果重启shell,改变就会失效。
进程间通信
由于每个进程都有独立的内存空间,要实现进程间的通信,必须要使用文件或者管道。
管道是由pipe函数创建:
#include <unistd.h>
int pipe(int fd[2]);
调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写端。进程使用以下方式使用管道进行通信。
- 进程调用
pipe
- 调用
fork()
,创建和父进程相同的子进程 - 父进程关闭读断(
fd[0]
),子进程关闭写端(fd[1]
),实现子进程接收父进程信息
具体代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
int n, fd[2];
pid_t pid;
char line[24];
if (pipe(fd) < 0)
perror("pipe error\n");
if ((pid = fork()) < 0)
perror("fork error\n");
/* parent process */
if (pid > 0) {
close(fd[0]);
write(fd[1], "wrote by parent process\n", 24);
}
/* child process */
else {
close(fd[1]);
read(fd[0], line, 24);
printf("print by child process: %s", line);
}
return 0;
}
运行程序,结果如下:
print by child process: wrote by parent process
子进程通过pipe,成功接收到了父进程的信息。