进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后
它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。
1. 进程的环境变量
每一个进程都有一组与其相关的环境变量(子进程会继承父进程),这些环境变量以字符串形式存储在一个字符串数组列表中,把这个数组称为环境列表。其中每个字符串都是以“名称=值(name=value)”形式定义。
1.1 environ
该变量是一个全局变量,指向的是当前进程的环境变量的数组, 数组末尾是一个NULL,所以可以用这个NULL判断数组的结束
#include <stdio.h> #include <stdlib.h> extern char **environ;
1.2 getenv()
获取指定name的value
#include <stdlib.h> char *getenv(const char *name);
返回值: name存在就返回对应字符串的指针,不存在就返回NULL
1.3 putenv()
增加环境变量
注意:该函数将设定的environ变量(字符串数组)中的某个元素(字符串指针)指向该string字符串,没有重新分配空间,不能随意修改参数 string 所指向的内容,这将影响进程的环境变量
#include <stdlib.h> int putenv(char *string);
1.4 setenv()
向环境变量列表中添加一个新的环境变量或修改现有的环境变量(该函数会分配空间的)
#include <stdlib.h> int setenv(const char *name, const char *value, int overwrite);
overwrite:若参数 name 标识的环境变量已经存在,在参数 overwrite 为 0 的情况下,setenv()函数将不
改变现有环境变量的值,也就是说本次调用没有产生任何影响;如果参数 overwrite 的值为非 0,若参数 name
- 标识的环境变量已经存在,则覆盖,不存在则表示添加新的环境变量。
1.5 unsetenv()
移除环境变量
#include <stdlib.h> int unsetenv(const char *name);
1.6 clearenv()
清空环境变量
#include<stdlib.h> int clearenv(void);
clearenv()函数内部的做法其实就是将environ赋值为NULL。在某些情况下,使用setenv()函数和clearenv()函数可能会导致程序内存泄漏,前面提到过,setenv()函数会为环境变量分配一块内存缓冲区,随之称为进程的一部分;
而调用 clearenv()函数时没有释放该缓冲区(clearenv()调用并不知晓该缓冲区的存在,故而也无法将其释放),反复调用者两个函数的程序,会不断产生内存泄漏。
2. 创建子进程
2.1 fork()
#include <unistd.h> pid_t fork(void);
- 将会在父进程中返回子进程的 PID,而在子进程中返回值是 0;如果调用失败,父进程返回值-1,不创建子进程,并设置 errno。
- 子进程,父进程都会接着fork()函数后面的代码运行。
- 子进程是父进程的一个副本,譬如子进程拷贝了父进程的数据段、堆、栈以及继承了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间,这是子进程对父进程相应部分存储空间的完全复制
- 父子进程共享代码段,在内存中只存在一份代码段数据。
- 父、子进程不应都使用 exit()终止,只能有一个进程使用 exit()、而另一个则使用_exit()退出,当然一般推荐的是子进程使用_exit()退出、而父进程则使用 exit()退出。, exit执行流程:
- 执行进程终止函数(如果注册了)
- 刷新stdio流缓冲
- 执行_exit()系统调用
- 父子进程的文件共享问题类似于dup(), 子线程仅仅是拷贝的文件描述符,没有拷贝文件表,所以他们的文件偏移指针是同一个。
3. 监视子进程
3.1 wait()
等待任一子进程终止,同时获得子进程的终止状态信息。
调用 wait()函数,如果其所有子进程都还在运行,则 wait()会一直阻塞等待,直到某一个子进程终
止;
如果进程调用 wait(),但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那 么 wait()将返回错误,也就是返回-1、并且会将 errno 设置为 ECHILD。
如果进程调用 wait()之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用 wait()也不会阻塞。wait()函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子进程的一些资源,俗称为子进程“收尸”。所以在调用 wait()函数之前,已经有子进程终止了,意味着正等待着父进程为其“收尸”,所以调用wait()将不会阻塞,而是会立即替该子进程“收尸”、处理它的“后事”,然后返回到正常的程序流程中,一次 wait()调用只能处理一次。
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
status: 存放子进程终止时的状态信息。可为NULL,表示不接收。可通过一些宏函数来判断子进程的结束状态
WIFEXITED(status): 如果子进程正常退出,则返回true
WEXITSTATUS(status): 返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit()时指定的退出状态
WIFSIGNALED(status):如果子进程被信号终止,则返回 true
WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号
WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;
返回值: 若成功则返回终止的子进程对应的进程号;失败则返回-1。
3.2 waitpid()
相比于wait()的优势:
- waitpid可以等待某个特定的子进程完成(参数pid)
- waitpid可以实现非阻塞等待
- 子进程因为信号而造成的暂停或恢复也能等待。
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
pid:
pid > 0, 表示等待进程号pid的子进程
pid = 0 , 表示等待与调用(父进程)同一个进程组的所有子进程。
pid < -1 , 表示等待进程组标示符与pid绝对值相等的所有子进程。
pid = -1, 等待任一子进程。
options是一个掩码位
WNOHANG: 如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待.。过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息
WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息
返回值: 返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0
较好的收尸逻辑: 因为子线程的结束或暂停或恢复会给父线程发送SIGCHLD信号,所以我们可以捕获该信号来收尸。
// 信号处理函数这样写 void func(int sig) { while (waitpid(-1, NULL, WNOHANG) > 0) { continue; } }
4. 执行新程序
4.1 execve()
通过调用 execve()函数将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆
数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行。
#include <unistd.h> int execve(const char *filename, char *const argv[], char *const envp[])
- filename: 可执行程序的地址,绝对地址或相对地址都可
- 参数 argv 则指定了传递给新程序的命令行参数。与main(int argn, char **argv)中的argv相同,包括第一个字符串是指令名,同时最后一个元素是NULL
- 参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表, 最后一个元素也必须是NULL
- 调用成功将不会返回;失败返回-1,并设置errno
4.2 其他类似execve()的函数
#include <unistd.h> extern char **environ; // l表示argv参数以列表形式传递,v表示argv参数以数组形式传递 // 如: execl("\bin\ls","ls","-l","-a") argv={"ls","-l","-a"};execl("\bin\ls",argv) int execl(const char *path, const char *arg, ... /* (char *) NULL */); int execv(const char *path, char *const argv[]); // p表示file可以只写文件名,让其到PATH环境变量里面去找 // 如"\bin\ls"中的\bin是在环境变量PATH的,所以可以直接传递"ls" int execlp(const char *file, const char *arg, ... /* (char *) NULL */); int execvp(const char *file, char *const argv[]); // e表示还需要传递环境变量数字 int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */); int execvpe(const char *file, char *const argv[], char *const envp[]);
4.3 system()
system()函数其内部的是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能,首先 system()会调用 fork()创建一个子进程来运行 shell(可以把这个子进程成为 shell 进程),并通过 shell 执行参数command 所指定的命令.
该函数至少会创建两个进程,顾效率不高。
#include <stdlib.h> int system(const char *command);
5. 进程关系
5.1 无关系
5.2 进程组
- 每个进程必定属于某个进程组,并且只能属于一个进程组
- 每个进程组有个组长进程,组长进程的ID就等于进程组ID
- 在组长进程的ID前面加上一个负号即是操作进程组
- 组长进程不能再创建新的进程组
- 只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关
- 一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离开该进程组
- 默认情况下,新创建的进程会继承父进程的进程组 ID
5.2.1 getpgid()
获取当前进程组ID
#include <unistd.h> pid_t getpgid(pid_t pid); // 可指定获取的线程,pid=0表示当前调用者 pid_t getpgrp(void); // getpgrp()等价于getpgid(0,0)
setpgid()函数将参数 pid 指定的进程的进程组 ID 设置为参数 gpid。如果这两个参数相等(pid==gpid),则由 pid 指定的进程变成为进程组的组长进程,创建了一个新的进程;如果参数 pid 等于 0,则使用调用者的进程 ID;另外,如果参数 gpid 等于 0,则创建一个新的进程组,由参数 pid 指定的进程作为进程组组长进程。
5.2.2 setpgid()
设置进程组ID
#include <unistd.h> int setpgid(pid_t pid, pid_t pgid); int setpgrp(void);
5.3 会话
会话是一个或多个进程组的集合,其与进程组、进程之间的关系如下图所示:
- 一个会话可以有控制终端、也可没有控制终端,在有控制终端的情况下也只能连接一个控制终端,这通常是登录到其上的终端设备。一个会话中的进程组可被分为一个前台进程组以及一个或多个后台进程组。
- 会话的首领进程连接一个终端之后,该终端就成为会话的控制终端,与控制终端建立连接的会话首领进程被称为控制进程;产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程,譬如 Ctrl + C(产生 SIGINT 信号)、Ctrl + Z(产生 SIGTSTP 信号)、Ctrl + \(产生 SIGQUIT 信号)等等这些由控制终端产生的信号。
5.1 getsid()
获取会话ID
#include <unistd.h> pid_t getsid(pid_t pid);
5.2 setsid()
如果调用者进程不是进程组的组长进程,调用 setsid()将创建一个新的会话,调用者进程是新会话的首领进程,同样也是一个新的进程组的组长进程,调用 setsid()创建的会话将没有控制终端。
#include <unistd.h> pid_t setsid(void);
5. 守护进程
守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:长期运行, 与控制终端脱离,即pid=gid=sid
5.1 编写守护进程步骤
- 创建子进程,终止父进程
- 子进程调用setsid创建会话(脱离原先的进程组和控制终端)
- 将工作目录改为自己的更目录
- 重新设置文件掩码umask
- 关闭不在需要的文件描述符
- 将文件描述符0,1,2定位到/dev/null
- 忽略SIGCHLD信号
5.2 代码实例
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<signal.h> int main(void) { pid_t pid; int i; int ret; // 1. 创建子进程 pid = fork(); if (pid < 0) { perror("fork error"); exit(-1); } else if (pid > 0) exit(0); // 父进程直接退出 // 子进程 // 2. 创建新的会话 ret = setsid(); if (ret < 0) { perror("setsid error"); exit(-1); } // 3. 改变工作目录 ret = chdir("./"); if (ret < 0) { perror("chdir error"); exit(-1); } // 4. 重新设置umask umask(0); // 5. 关闭所有不需要的文件 for(i=0;i<sysconf(_SC_OPEN_MAX);i++) close(i); // 6. 重定位0,1,2 open("/dev/null", O_RDWR); dup(0); dup(0); // 7. 忽略SIGCHLD信号 signal(SIGCHLD, SIG_IGN); // 守护进程逻辑 i = 0; for(;;) { sleep(1); printf("守护进程运行中....%d\n", i++); } exit(0); }
6. 其他
6.1 atexit()
注册一个进程在正常终止时要调用的函数。如果程序当中使用了_exit()或_Exit()终止进程而并非是 exit()函数,那么将不会执行注册的终止处理函数
#include <stdlib.h> int atexit(void (*function)(void));
6.2 getpid()
获取当前进程的PID
#include <sys/types.h> #include <unistd.h> pid_t getpid(void);
6.3 getppid()
获取当前进程父进程的PID
#include <sys/types.h> #include <unistd.h> pid_t getppid(void);
6.4 单例模式运行
保证系统中该程序只能有一个进程,使用文件锁的方式。
#include <stdio.h> #include <stdlib.h> #include <sys/file.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <string.h> #define LOCK_FILE "./testApp.pid" int main(void) { char str[20] = {0}; int fd; /* 打开 lock 文件,如果文件不存在则创建 */ fd = open(LOCK_FILE, O_WRONLY | O_CREAT, 0666); if (-1 == fd) { perror("open error"); exit(-1); } // 以非阻塞的方式获取文件锁 if (-1 == flock(fd, LOCK_EX|LOCK_NB)){ fprintf(stderr, "不能重复执行该程序\n"); close(fd); exit(-1); } puts("程序运行中....\n"); ftruncate(fd, 0); sprintf(str, "%d\n", getpid()); write(fd, str, strlen(str)); for(;;) sleep(1); exit(0); }