在操作系统和应用程序之间的接口处,应用程序可以创建子进程,等待它们的子进程停止或终止,运行新的程序,发送信号到其他进程以及捕获(处理)来自其他进程的信号。

获取进程ID

每个进程都有唯一的进程ID(PID为正数),getpid返回调用它的进程的PID,getppid返回它的父进程的PID。

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

pid_t在Linux系统上在types.h中定义为int

创建和终止进程

从程序员的角度,我们可以认为进程总是处于下面三种状态之一:

  • 运行: 进程要么在CPU上执行,要么在等待被执行而且最终会被内核调度。
  • 停止: 进程的执行被挂起(suspend),而且不会被调度。当收到SIGSTOPSIGTSTPSIDTTINSIGTTOU信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行。
  • 终止: 进程永远的停止了。进程会因为三种原因而终止:
    1. 收到一个信号,该信号的默认行为是终止进程;
    2. 从主程序返回;
    3. 调用exit函数。
#include <stdlib.h>
void exit(int status);

exit函数以status退出状态来终止进程(另一种退出状态的方法是从主程序中return一个整数值)。

父进程通过调用fork来创建一个新的子进程:

#include <unistd.h>

pid_t fork(void);

新创建的子进程得到与父进程用户级虚拟地址空间相同但独立的一份拷贝(包括文本,数据和bss段,堆栈以及用户栈),子进程还获得与父进程任何打开文件描述符相同的拷贝,这意味着子进程可以读写父进程中打开的任何文件。父子进程的最大区别在于他们有不同的PID。

fork函数调用一次,但返回两次,一次在父进程,一次在新创建的子进程。在父进程中,fork返回子进程的PID(任何PID都为正数),在子进程中,fork返回为0(这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID)。

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(){
    int ppid=getpid();
    int x=1;
    printf("Process (%d) start....\n", ppid);
    int cpid=fork();
    if (cpid == 0) {
        x++;
        printf("I am child process (%d) and my parent is (%d); [x=%d].\n",
               getpid(), getppid(), x);
        exit(1);
    } else {
        x--;
        printf("I (%d) just created a child process (%d); [x=%d].\n",
              getpid(), cpid, x);
    }
    return 0;
}

输出:

Process (27228) start....
I (27228) just created a child process (27056); [x=0].
I am child process (27056) and my parent is (27228); [x=2].

x的值可以看出,父子进程有相同但独立的地址空间

父子进程是并发运行的独立进程,内核能以任意方式交替执行它们的逻辑控制流中的指令,也就是说,执行结果是不确定的

有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。

以下是利用fork可以完成一个简单的并发服务器轮廓:

pid_t pid;
int listenfd, connfd;
listenfd = socket(...);  /* fill in sockaddr_in{} with server's well-known port */
bind(listenfd, ...);
listen(listenfd, LISTENQ);
for ( ; ; ) {
    connfd = accept(listenfd, ...);  /* probably blocks */
    if ( (pid = fork()) == 0) {
        close(listenfd);  /* child close listening socket */
        doit(connfd);  /* process the request */
        close(connfd);  /* child terminates */
        exit(0);
    }
    close(connfd);  /* parent closes connected socket */
}

回收子进程

一个进程通过调用waitpid函数来等待它的子进程来终止或者停止

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
pid_t wait(int *status); // wait(&status) 等价于 waitpid(-1, &status, 0)
  1. pid决定等待集合:
    • 如果pid > 0,那么等待集合就是一个单独的子进程,它的进程ID等于pid
    • 如果pid = -1, 那么等待集合就是父进程的所有子进程。
  2. 可以通过status检查已回收子进程的退出状态,如果status参数是非空的,那么waitpid就会在status参数中放上关于导致返回的子进程的状态信息。wait.h头文件定义解释status参数的几个宏。
    • WIFEXITED: 如果子进程通过调用exit或者一个返回(return)正常终止,就返回真。
    • WEXITSTATUS: 返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态。
    • WIFSIGNALED: 如果子进程是因为一个未被捕捉的信号终止时,那么就返回为真。
    • WTERMSIG: 返回导致子进程终止的信号的编号。只有当WIFSIGNALED返回为真时,才定义这个状态。
    • WIFSTOPPED: 如果引起返回的子进程当前是被停止的,那么就返回为真。
    • WSTOPSIG: 返回引起子进程停止的信号的数量。只有当WIFSTOPED返回为真时,才定义这个状态。
  3. options决定waitpid的行为:
    • options=0(默认): waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。如果等待集合一个进程在刚刚调用的时刻就已经终止了, waitpid就立即返回(即父子进程同步执行)。在这两种情况下,waitpid返回导致waitpid返回的已终止的子进程的PID,并将这个已终止的子进程从系统中去除。
    • WNOHANG: 如果等待集合中任何子进程都还没有终止,那么就立即返回(返回值为0),而不是挂起调用进程。(即父子进程异步执行,子进程终止时内核发送SIGCHLD给父进程,而在等待子进程终止的这段时间内,父进程还可以工作)
    • WUNTRACED: 挂起调用进程的执行,直到等待集合的一个进程变成已经终止或者被停止waitpid返回导致waitpid返回的已终止或停止的子进程的PID(与默认行为不同,它还检查停止的子进程)。
    • WNOHANG|WUNTRACED: 立即返回,如果等待集合中没有任何子进程被停止或已终止,返回0,或者返回那个被停止或已经终止的子进程的PID。
  4. 错误条件:
    • 如果调用进程没有子进程,那么waitpid返回-1,并设置errnoECHILD
    • 如果waitpid被一个信号中断,那么它返回-1,并设置errnoEINTR

让进程休眠

sleep将一个进程挂起一段指定的时间。

#include <unistd.h>
unsigned int sleep(unsigned int secs);

如果请求的时间量已经到了,sleep返回0,否则返回还剩下的要休眠的秒数(例如sleep函数被一个信号中断而过早的返回)

可以编写包装函数snooze,除了会打印一条信息来描述进程的实际休眠时间,和sleep的行为一样

unsigned int snooze(unsigned int secs){
    unsigned int rc = sleep(secs);
    printf("slept for %u of %u secs.", secs-rc, secs);
    return rc;
}

pause让函数休眠,直到该进程收到一个信号。

#include <unistd.h>
int pause(void);

加载并运行程序

execve在当前进程的上下文中加载并运行一个新程序。

#include <unistd.h>
int execve(const char *pathname, const char *argv[], const char *envp[])

execve加载并运行可执行目标文件pathname,并带参数列表argv和环境变量列表envp。只有出现错误,例如找不到pathnameexecve才会返回到调用程序。所以,execve调用一次并从不返回。

环境变量

#include <stdlib.h>

// 若存在name则返回指向value的指针,若无匹配,则返回NULL(环境变量name=value)
char *getenv(const char *name);

// 若成功则为0,若错误则为-1
int setenv(const char *name, const char *newvalue, int overwrite);

void unsetenv(const char *name);