本文简单说明了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个用于进程控制的主要函数:forkwaitpid。我们用一个小程序来说明他们的具体作用:

#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是40461ps命令显示僵尸进程的状态为ZN。

如果一个父进程终止,而它的子进程还存在(这些子进程或者仍在运行,或者已经是僵尸进程了),则这些子进程的父进程改为init进程。init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止,init就会调用wait函数清理它。

环境变量

每个程序都有一张环境表,环境表是一个字符串指针数组。全局变量environ包含了该指针数组的地址。

environment list

由于我们的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,成功接收到了父进程的信息。