1 进程概念
1.1进程号
ps -aux //查看进程的相关信息
应用程序中获取进程ID号:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
pid_t pid; // 声明一个 pid_t 类型的变量,用于存储进程ID(Process ID)
pid = getpid(); // 获取当前进程的进程ID 并存储在 pid 变量中
pid = getppid(); //获取父进程 pid 并存储在 pid 变量中
pid = getsid(0); //0:获取调用者进程的会话 ID;非0:获取指定的进程(非0值)对应的会话 ID
1.2 进程的环境变量
// 在Ubuntu系统的shell 终端下输入代码:
env // 查看 shell 进程的所有环境变量
export LINUX_APP=123456 # 添加 LINUX_APP 环境变量
export -n LINUX_APP # 删除 LINUX_APP 环境变量
应用程序中获进程的全部环境变量:
#include <stdio.h>
#include <stdlib.h>
extern char **environ; // 申明外部全局变量 environ
int main(int argc, char *argv[])
{
int i;
/* 打印进程的环境变量 */
for (i = 0; NULL != environ[i]; i++) // 通过字符串数组元素是否等于 NULL 来判断是否已经到了数组的末尾。
puts(environ[i]); // 输出进程的环境变量
exit(0); // 0 代表程序正常退出;非0值 代表程序异常退出
}
2 进程的创建
2.1 fork() 创建子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid; // 声明一个 pid_t 类型的变量
pid = fork(); // 创建子进程
switch (pid) {
case -1:
perror("fork error");
exit(-1);
case 0: // 子进程 返回 pid==0
printf("这是子进程打印信息<pid: %d, 父进程 pid: %d>\n",
getpid(), getppid());
_exit(0); //子进程使用_exit()退出
default: // 父进程返回子进程 pid
printf("这是父进程打印信息<pid: %d, 子进程 pid: %d>\n",
getpid(), pid);
exit(0);
}
}
2.2 vfork() 创建子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
pid_t pid;
int num = 100;
pid = vfork(); // 创建子进程
switch (pid) {
case -1:
perror("vfork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程打印信息\n");
printf("子进程打印 num: %d\n", num);
_exit(0);
default:
/* 父进程 */
printf("父进程打印信息\n");
printf("父进程打印 num: %d\n", num);
exit(0);
}
}
2.3 fork() 与 vfork() 区别
- 调用 fork () 之后,将产生竞争条件,无法确定父、子两个进程谁将率先访问 CPU;如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它。
- vfork() 保证子进程先运行,子进程调用 exec 之后父进程才可能被调度运行。
- 当子进程并不需要用到父进程的数据段、堆段、栈段(譬如父程序中定义的局部变量、全局变量等)中的数据时,vfork()系统调用在效率上要优于 fork()
- 虽然 vfork()系统调用在效率上要优于 fork(),但是 vfork()可能会导致一些难以察觉的程序 bug,所以尽量避免使用 vfork()来创建子进程。
3 监视子进程
3.1 wait() 函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- status:参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。
- 返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1。
- 调用 wait()函数,如果其所有子进程都还在运行,则 wait()会一直阻塞等待,直到某一个子进程终止;
-
如果进程调用 wait() ,但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那么 wait() 将返回错误,也就是返回 -1 、并且会将 errno 设置为 ECHILD 。
- 如果进程调用 wait()之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用 wait() 也不会阻塞。wait()函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子进程的一些资源,俗称为子进程“收尸”。
- WIFEXITED(status):如果子进程正常终止,则返回 true;
- WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit() 时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过WEXITSTATUS 宏转换;
- WIFSIGNALED(status):如果子进程被信号终止,则返回 true;
- WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
- WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;
限制:
- 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
- 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
- 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。
3.2 waitpid() 函数
waitpid() 可以突破 wait() 函数中的限制;waitpid()系统调用函数原型如下所示:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
pid:参数 pid 用于表示需要等待的某个具体子进程,关于参数 pid 的取值范围如下:
- 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
- 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
- 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程; 如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。
status:与 wait()函数的 status 参数意义相同。
- WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
- WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进 程状态信息;
- WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。
返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0
3.3 waitid() 函数
4 僵尸进程与孤儿进程
4.1 孤儿进程
- 父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程。
- 在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程
4.2 僵尸进程
- 当父进程调用 wait()(或其变体)为子进程“收尸”后,僵尸进程就会被内核彻底删除。
- 如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(),故而从系统中移除僵尸进程。
-
僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死。只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!
4.3 及时回收僵尸进程:SIGCHLD 信号
- 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
- 当父进程的某个子进程因收到信号而发生状态改变时(终止、暂停或恢复),父进程会收到 SIGCHLD 信号。
while (waitpid(-1, NULL, WNOHANG) > 0) // 代码一直循环下去,直至 waitpid()返回 0,表明再无僵尸进程存在;或者返回-1,表明有错误发生。
continue;
5 在旧程序代码中执行另外一个编写的新程序代码(一般用于编写子进程)
5.1 execve() 函数
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
- filename:参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
- argv:参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main(int argc, char *argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。 argv[0]对应的便是新程序自身路径名。
- envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程 序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式name=value。
- 返回值:execve 调用成功将不会返回;失败将返回-1,并设置 errno。
6 system() 函数
使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令,system()函数原型如下所示:
#include <stdlib.h>
int system(const char *command);
- command:参数 command 指向需要执行的 shell 命令,以字符串的形式提供,譬如"ls -al"、"echo HelloWorld"等。
返回值:
1. 如果 参数 command 为 NULL,如果 shell 可用则返回一个非 0 值,若不可用则返回 0;
- 如果无法创建子进程或无法获取子进程的终止状态,那么 system()返回-1;
- 如果子进程不能执行 shell,则 system()的返回值就好像是子进程通过调用_exit(127)终止了
- 如果所有的系统调用都成功,system()函数会返回执行 command 的 shell 进程的终止状态。
#include <stdlib.h>
system("ls -la")
system("echo HelloWorld")
7 进程状态
- 就绪态(Ready):指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到CPU 就能够直接运行;
- 运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态;
-
僵尸态:僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”;
-
可中断睡眠状态(浅度睡眠):表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒;
-
不可中断睡眠状态(深度睡眠):深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。
-
暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 SIGSTOP 信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号。
图7.1 进程各状态之间的切换
8 进程间通信
进程间通信(interprocess communication,简称 IPC)指两个进程之间的通信。系统中的每一个进程都有各自的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。
8.1 管道(pipe)
把一个进程连接到另一个进程的数据流称为管道,管道被抽象成一个文件(pipe)。
管道包括三种:
- 普通管道 pipe:通常有两种限制,一是单工,数据只能单向传输;二是只能在父子或者兄弟进程间使用;
- 流管道 s_pipe:去除了普通管道的第一种限制,为半双工,可以双向传输;只能在父子或兄弟进程间使用;
- 有名管道 name_pipe(FIFO):去除了普通管道的二种限制,并且允许在不相关(不是父子或兄 弟关系)的进程间进行通讯。
8.2 信号
用于通知接收信号的进程有某种事件发生,所以可 用于进程间通信;除了用于进程间通信之外,进程还可以发送信号给进程本身。
8.3 消息队列
消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少、管 道只能承载无格式字节流以及缓冲区大小受限等缺陷。
消息队列是 UNIX 下不同进程之间实现共享资源的一种机制,UNIX 允许不同进程将格式化的数据流以 消息队列形式发送给任意进程,有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读 走队列中的消息。
8.4 信号量
信号量是一个计数器,信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
为了防止多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种访问机制,它可以通过生成并使用令牌来授权,在同一时刻只能有一个线程访问代码的临界区域。
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
8.5 共享内存
共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但其它的多个进程都可以访问,使得多个进程可以访问同一块内存空间。
共享内存是最快的 IPC 方式,它是针对其它进程间 通信方式运行效率低而专门设计的,它往往与其它通信机制,譬如结合信号量来使用,以实现进程间的同步和通信。
8.6 套接字(Socket)
Socket 是一种基于网络的 IPC 方法,允许位于同一主机(计算机)或使用网络连接起来 的不同主机上的应用程序之间交换数据,说白了就是网络通信。
在典型的客户端/服务器场景中,应用程序使用 socket 进行通信的方式如下:
- 各个应用程序创建一个 socket。socket 是一个允许通信的“设备”,两个应用程序都需要用到它。
- 服务器将自己的 socket 绑定到一个众所周知的地址(名称)上使得客户端能够定位到它的位置。