一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程
进程其实就是一个可执行程序的实例
程序如何结束?
程序结束其实就是进程终止,进程终止的方式通常有多种,大体上分为正常终止和异常终止,正常终止 包括:
⚫
main()
函数中通过
return
语句返回来终止进程;
⚫
应用程序中调用
exit()
函数终止进程;
⚫
应用程序中调用
_exit()
或
_Exit()
终止进程;
以上这些是在前面的课程中给大家介绍的,异常终止包括:
⚫
应用程序中调用
abort()
函数终止进程;
⚫
进程接收到一个信号,譬如
SIGKILL
信号。
注册进程终止处理函数
atexit()
#include <stdlib.h>int atexit(void (*function)(void));使用该函数需要包含头文件 <stdlib.h> 。function : 函数指针,指向注册的函数,此函数无需传入参数、无返回值。返回值: 成功返回 0 ;失败返回非 0 。
#include <stdio.h>
#include <stdlib.h>
static void bye(void)
{
puts("Goodbye!");
}
int main(int argc, char *argv[])
{
if (atexit(bye)) {
fprintf(stderr, "cannot set exit function\n");
exit(-1);
}
exit(0);
}
进程号
Linux
系统下的每一个进程都有一个进程号(
processID
,简称
PID
),进程号是一个正数,用于唯一标识系统中的某一个进程。在 Ubuntu
系统下执行
ps
命令可以查到系统中进程相关的一些信息,包括每个进程的进程号
在应用程序中,可通过系统调用
getpid()
来获取本进程的进程号,
getpid()函数
#include <sys/types.h>#include <unistd.h>pid_t getpid(void);使用该函数需要包含头文件 <sys/types.h> 和 <unistd.h> 。函数返回值为 pid_t 类型变量,便是对应的进程号。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid = getpid();
printf("本进程的 PID 为: %d\n", pid);
exit(0);
}
进程的环境变量
每一个进程都有一组与其相关的环境变量,这些环境变量以字符串形式存储在一个字符串数组列表中, 把这个数组称为环境列表。其中每个字符串都是以“名称=值(
name=value
)”形式定义,所以环境变量是 “名称-
值”的成对集合,譬如在
shell 终端下可以使用 env 命令查看到 shell 进程的所有环境变量
使用 export 命令还可以添加一个新的环境变量或删除一个环境变量:export LINUX_APP=123456 # 添加 LINUX_APP 环境变量使用 "export -n LINUX_APP" 命令则可以删除 LINUX_APP 环境变量。export -n LINUX_APP # 删除 LINUX_APP 环境变量
应用程序中获取环境变量
在应用程序中,通过
environ
变量指向它,
environ
是一个全局变量,在我们的应用程序中只需申明它即可使用
extern char **environ; // 申明外部全局变量 environ
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[])
{
int i;
/* 打印进程的环境变量 */
for (i = 0; NULL != environ[i]; i++)
puts(environ[i]);
exit(0);
}
获取指定环境变量
getenv()
如果只想要获取某个指定的环境变量,可以使用库函数
getenv()
添加
/
删除
/
修改环境变量
C
语言函数库中提供了用于修改、添加、删除环境变量的函数,譬如
putenv()
、
setenv()
、
unsetenv()
、 clearenv()函数等。
putenv()函数-添加,setenv()函数-添加(推荐),unsetenv()
函数-移除环境变量
fork()创建子进程(重点)
在诸多的应用中,创建多个进程是任务分解时行之有效的方法,譬如,某一网络服务器进程可在监听客户端请求的同时,为处理每一个请求事件而创建一个新的子进程,与此同时,服务器进程会继续监听更多的客户端连接请求。在一个大型的应用程序任务中,创建子进程通常会简化应用程序的设计,同时提高了系统的并发性(即同时能够处理更多的任务或请求,多个进程在宏观上实现同时运行)。
调用
fork()
函数的进程称为父进程,由
fork()
函数创建出来的进程被称为子进程
#include <unistd.h>pid_t fork(void);使用该函数需要包含头文件 <unistd.h> 。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid = fork();
switch (pid) {
case -1:
perror("fork error");
exit(-1);
case 0:
printf("这是子进程打印信息<pid: %d, 父进程 pid: %d>\n",
getpid(), getppid());
_exit(0); //子进程使用_exit()退出
default:
printf("这是父进程打印信息<pid: %d, 子进程 pid: %d>\n",
getpid(), pid);
exit(0);
}
}
//case 0 是子进程的分支,这里使用了_exit()结束进程而没有使用 exit()。
例子2
在
exit()
函数之前添加了打印信息,而从上图中可以知道,打印的
pid
值并不相同,
0
表示子进程打印出来的,46953
表示的是父进程打印出来的,所以从这里可以证实,
fork()
函数调用完成之后,父进程、子进程会各自继续执行 fork()
之后的指令,它们共享代码段,但并不共享数据段、堆、栈等,而是子进程拥有父进程数据段、堆、栈等副本,所以对于同一个局部变量,它们打印出来的值是不相同的,因为 fork()
调用返回值不同,在父、子进程中赋予了 pid
不同的值。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid = fork();
switch (pid) {
case -1:
perror("fork error");
exit(-1);
case 0:
printf("这是子进程打印信息\n");
printf("%d\n", pid);
_exit(0);
default:
printf("这是父进程打印信息\n");
printf("%d\n", pid);
exit(0);
}
}
除了
fork()
系统调用之外,
Linux
系统还提供了
vfork()
系统调用用于创建子进程
#include <sys/types.h>#include <unistd.h>pid_t vfork(void);使用该函数需要包含头文件 <sys/types.h> 和 <unistd.h> 。
父、子进程间的文件共享
调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于 dup(), 这也意味着父、子进程对应的文件描述符均指向相同的文件表
例程1:父进程打开文件之后,然后 fork()创建子进程,此时子进程继承了父进程打开的文件描述符(父进程文件描述符的副本),然后父、子进程同时对文件进行写入操作
上述代码中,父进程
open
打开文件之后,才调用
fork()
创建了子进程,所以子进程了继承了父进程打 开的文件描述符 fd
,我们需要验证的便是两个进程对文件的写入操作是分别各自写入、还是每次都在文件末尾接续写入。
父、子进程分别对同一个文件进行写入操作,结果是接续写,不管是父进程,还是子进程,在每次写入时都是从文件的末尾写入
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
pid_t pid;
int fd;
int i;
fd = open("./test.txt", O_RDWR | O_TRUNC);
if (0 > fd) {
perror("open error");
exit(-1);
}
pid = fork();
switch (pid) {
case -1:
perror("fork error");
close(fd);
exit(-1);
case 0:
/* 子进程 */
for (i = 0; i < 4; i++) //循环写入 4 次
write(fd, "1122", 4);
close(fd);
_exit(0);
default:
/* 父进程 */
for (i = 0; i < 4; i++) //循环写入 4 次
write(fd, "AABB", 4);
close(fd);
exit(0);
}
}
例程2:
再来测试另外一种情况,父进程在调用
fork()
之后,此时父进程和子进程都去打开同一个文件,然后再对文件进行写入操作
这种文件共享方式实现的是一种两个进程分别各自对文件进行写入操作,因为父、子
进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
pid_t pid;
int fd;
int i;
pid = fork();
switch (pid) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
fd = open("./test.txt", O_WRONLY);
if (0 > fd) {
perror("open error");
_exit(-1);
}
for (i = 0; i < 4; i++) //循环写入 4 次
write(fd, "1122", 4);
close(fd);
_exit(0);
default:
/* 父进程 */
fd = open("./test.txt", O_WRONLY);
if (0 > fd) {
perror("open error");
exit(-1);
}
for (i = 0; i < 4; i++) //循环写入 4 次
write(fd, "AABB", 4);
close(fd);
exit(0);
}
}
fork()函数使用场景
⚫
父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用 fork()
创建一个子进程,使子进程去处理此请求、而父进程可以继续等待下一个服务请求。
⚫
一个进程要执行不同的程序。譬如在程序
app1
中调用
fork()
函数创建了子进程,此时子进程是要去执行另一个程序 app2
,也就是子进程需要执行的代码是
app2
程序对应的代码,子进程将从
app2程序的 main
函数开始运行。这种情况,通常在子进程从
fork()
函数返回之后立即调用
exec
族函数来实现,关于 exec
函数将在后面内容向大家介绍。
fork()之后的竞争条件
调用 fork()之后,子进程成为了一个独立的进程,可被系统调度运行,而父进程也继续被系统调度运行,
这里出现了一个问题,调用 fork
之后,无法确定父、子两个进程谁将率先访问
CPU
,也就是说无法确认谁先被系统调用运行(在多核处理器中,它们可能会同时各自访问一个 CPU
),这将导致谁先运行、谁后运行这个顺序是不确定的
为了保证某一特定执行顺序:可以通过采用采用某种同步技术来实现,譬如前面介绍的信号,如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它,示例代码如下所示
示例代码为子进程先运行打印相应信息,之后再执行父进程打印信息,在父进程分支中,直接调用了 sigsuspend()使父进程进入挂起状态,由子进程通过 kill 命令发送信号唤醒
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
static void sig_handler(int sig)
{
原子哥在线教学:www.yuanzige.com 论坛:http://www.openedv.com/forum.php
331
I.MX6U 嵌入式 Linux C 应用编程指南
printf("接收到信号\n");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t wait_mask;
/* 初始化信号集 */
sigemptyset(&wait_mask);
/* 设置信号处理方式 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGUSR1, &sig, NULL)) {
perror("sigaction error");
exit(-1);
}
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程开始执行\n");
printf("子进程打印信息\n");
printf("~~~~~~~~~~~~~~~\n");
sleep(2);
kill(getppid(), SIGUSR1); //发送信号给父进程、唤醒它
_exit(0);
default:
/* 父进程 */
if (-1 != sigsuspend(&wait_mask))//挂起、阻塞
exit(-1);
printf("父进程开始执行\n");
printf("父进程打印信息\n");
exit(0);
}
}
监视子进程
在很多应用程序的设计中,父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视
wait()
函数
#include <sys/types.h>#include <sys/wait.h>pid_t wait(int *status);使用该函数需要包含头文件 <sys/types.h> 和 <sys/wait.h> 。status : 参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL ,表示不接收子进程终止时的状态信息。返回值: 若成功则返回终止的子进程对应的进程号;失败则返回 -1 。
⚫
调用
wait()
函数,如果其所有子进程都还在运行,则
wait()
会一直阻塞等待,直到某一个子进程终止;
⚫
如果进程调用
wait()
,但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那么 wait()
将返回错误,也就是返回
-1
、并且会将
errno
设置为
ECHILD
。
⚫
如果进程调用
wait()
之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用
wait() 也不会阻塞。wait()
函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子 进程的一些资源,俗称为子进程“收尸”,关于这个问题后面再给大家进行介绍。所以在调用 wait() 函数之前,已经有子进程终止了,意味着正等待着父进程为其“收尸”,所以调用 wait()
将不会阻塞,而是会立即替该子进程“收尸”、处理它的“后事”,然后返回到正常的程序流程中,一次 wait() 调用只能处理一次。
参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,可以通过以下宏来检查 status 参数:
⚫
WIFEXITED(status)
:
如果子进程正常终止,则返回
true
;
⚫
WEXITSTATUS(status)
:
返回子进程退出状态,是一个数值,其实就是子进程调用
_exit()
或
exit() 时指定的退出状态;wait()
获取得到的
status
参数并不是调用
_exit()
或
exit()
时指定的状态,可通过 WEXITSTATUS 宏转换;
⚫
WIFSIGNALED(status)
:
如果子进程被信号终止,则返回
true
;
⚫
WTERMSIG(status)
:
返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
⚫
WCOREDUMP(status)
:
如果子进程终止时产生了核心转储文件,则返回
true
;
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main(void)
{
int status;
int ret;
int i;
/* 循环创建 3 个子进程 */
for (i = 1; i <= 3; i++) {
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(i);
_exit(i);
default:
/* 父进程 */
break;
}
}
sleep(1);
printf("~~~~~~~~~~~~~~\n");
for (i = 1; i <= 3; i++) {
ret = wait(&status);
if (-1 == ret) {
if (ECHILD == errno) {
printf("没有需要等待回收的子进程\n");
exit(0);
}
else {
perror("wait error");
exit(-1);
}
}
printf("回收子进程<%d>, 终止状态<%d>\n", ret,
WEXITSTATUS(status));
}
exit(0);
}
waitpid()
函数
使用
wait()
系统调用存在着一些限制,这些限制包括如下:
⚫
如果父进程创建了多个子进程,使用
wait()
将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
⚫
如果子进程没有终止,正在运行,那么
wait()
总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
⚫
使用
wait()
只能发现那些被终止的子进程,对于子进程因某个信号(譬如
SIGSTOP
信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT
信号后恢复执行的情况就无能为力了。
#include <sys/types.h>#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);使用该函数需要包含头文件 <sys/types.h> 和 <sys/wait.h> 。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main(void)
{
int status;
int ret;
int i;
/* 循环创建 3 个子进程 */
for (i = 1; i <= 3; i++) {
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(i);
_exit(i);
default:
/* 父进程 */
break;
}
}
sleep(1);
printf("~~~~~~~~~~~~~~\n");
for (i = 1; i <= 3; i++) {
ret = waitpid(-1, &status, 0);
if (-1 == ret) {
if (ECHILD == errno) {
printf("没有需要等待回收的子进程\n");
exit(0);
}
else {
perror("wait error");
exit(-1);
}
}
printf("回收子进程<%d>, 终止状态<%d>\n", ret,
WEXITSTATUS(status));
}
exit(0);
}
特殊进程:僵尸进程、孤儿进程
⚫
父进程先于子进程结束。
⚫
子进程先于父进程结束
父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程。在 Linux
系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1
)的子进程,换言之,某一子进程的父进程结束后,该子进程调用 getppid()
将返回
1
,
init
进程变成了孤儿进程的“养父”;这是判定某一子进程的“生父”是否还“在世”的方法之一
判定某一子进程的“生父”是否还“在世”
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
/* 创建子进程 */
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建, 父进程<%d>\n", getpid(), getppid());
sleep(3); //休眠 3 秒钟等父进程结束
printf("父进程<%d>\n", getppid());//再次获取父进程 pid
_exit(0);
default:
/* 父进程 */
break;
}
sleep(1);//休眠 1 秒
printf("父进程结束!\n");
exit(0);
}
在上述代码中,子进程休眠
3
秒钟,保证父进程先结束,而父进程休眠
1
秒钟,保证子进程能够打印出第一个 printf()
,也就是在父进程结束前,打印子进程的父进程进程号;子进程
3
秒休眠时间过后,再次打印父进程的进程号,此时它的“生父”已经结束了。
![](https://i-blog.csdnimg.cn/blog_migrate/8cb85768ec7fbeec2bd99632128e6a71.png)
僵尸进程
进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用
wait()
(或其变体
waitpid()
、
waitid()
等)函数回收子进程资源,归还给系统。
如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。子进程结束后其父进程并没有来得及立马给它“收尸”,子进程处于“曝尸荒野”的状态,在这么一个状态下,我们就将子进程成为僵尸进程
当父进程调用
wait()
(或其变体,下文不再强调)为子进程“收尸”后,僵尸进程就会被内核彻底删除。 另外一种情况,如果父进程并没有调用 wait()
函数然后就退出了,那么此时
init
进程将会接管它的子进程并自动调用 wait()
,故而从系统中移除僵尸进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
/* 创建子进程 */
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(1);
printf("子进程结束\n");
_exit(0);
default:
/* 父进程 */
break;
}
for ( ; ; )
sleep(1);
exit(0);
}
在上述代码中,子进程已经退出,但其父进程并没调用
wait()
为其“收尸”,使得子进程成为一个僵尸进程,使用命令"ps -aux"
可以查看到该僵尸进程
![](https://i-blog.csdnimg.cn/blog_migrate/a474f21ae68639715a32269e0dc753d5.png)
创建新进程后,执行新程序
当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序的代码,那么这个时候子进程可以通过 exec
函数来实现运行另一个新的程序。本小节我们就来学习下, 如何在程序中运行一个新的程序,从新程序的 main()
函数开始运行
execve()
函数
#include <unistd.h>int execve(const char *filename, char *const argv[], char *const envp[]);使用该函数需要包含头文件 <unistd.h> 。filename : 参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。argv : 参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main(int argc, char *argv[])函数的第二个参数 argv ,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。 argv[0]对应的便是新程序自身路径名。envp : 参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value 。返回值: execve 调用成功将不会返回;失败将返回 -1 ,并设置 errno 。
编写一个简单地程序,在测试程序 testApp 当中通过 execve()函数运行另一个新程序 newApp。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char *arg_arr[5];
char *env_arr[5] = {"NAME=app", "AGE=25",
"SEX=man", NULL};
if (2 > argc)
exit(-1);
arg_arr[0] = argv[1];
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execve(argv[1], arg_arr, env_arr);
perror("execve error");
exit(-1);
}
将上述程序编译成一个可执行文件
testApp
。
接着编写新程序,在新程序当中打印出环境变量和传参,如下所示
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[])
{
char **ep = NULL;
int j;
for (j = 0; j < argc; j++)
printf("argv[%d]: %s\n", j, argv[j]);
puts("env:");
for (ep = environ; *ep != NULL; ep++)
printf(" %s\n", *ep);
exit(0);
}
exec 库函数
exec
族函数包括多个不同的函数,这些函数命名都以
exec
为前缀,上一小节给大家介绍的
execve()
函数也属于 exec
族函数中的一员,但它属于系统调用;本小节我们介绍
exec
族函数中的库函数,这些库函数都是基于系统调用 execve()
而实现的,虽然参数各异、但功能相同,包括:
execl()
、
execlp()
、
execle()
、
execv()
、execvp()、
execvpe()
,它们的函数原型如下所示。
进程状态
Linux
系统下进程通常存在
6
种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
⚫
就绪态(
Ready
):指该进程满足被
CPU
调度的所有条件但此时并没有被调度执行,只要得到
CPU 就能够直接运行;意味着该进程已经准备好被 CPU
执行,当一个进程的时间片到达,操作系统调度程序会从就绪态链表中调度一个进程;
⚫
运行态:指该进程当前正在被
CPU
调度运行,处于就绪态的进程得到
CPU
调度就会进入运行态;
⚫
僵尸态:僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”;
⚫
可中断睡眠状态:可中断睡眠也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可
以通过信号来唤醒;
⚫
不可中断睡眠状态:不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。把浅度睡眠和深度睡眠统称为等待态(或者叫阻塞态),表示进程处于一种等待状态,等待某种条件成立之后便会进入到就绪态;所以,处于等待态的进程是无法参与进程系统调度的。
⚫
暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如
SIGSTOP信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT
信号。
![](https://i-blog.csdnimg.cn/blog_migrate/de5db54c21bd1181cc9f6fd13491f62f.png)
进程关系
1
、无关系
两个进程间没有任何关系,相互独立。
2
、父子进程关系
两个进程间构成父子进程关系,譬如一个进程
fork()
创建出了另一个进程,那么这两个进程间就构成了父子进程关系,调用 fork()
的进程称为父进程、而被
fork()
创建出来的进程称为子进程;当然,如果“生父” 先与子进程结束,那么 init
进程(“养父”)就会成为子进程的父进程,它们之间同样也是父子进程关系。
3
、进程组
每个进程除了有一个进程
ID
、父进程
ID
之外,还有一个进程组
ID
,用于标识该进程属于哪一个进程组,进程组是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或者在功能上有联系。
关于进程组需要注意以下以下内容:
⚫
每个进程必定属于某一个进程组、且只能属于一个进程组;
⚫
每一个进程组有一个组长进程,组长进程的
ID
就等于进程组
ID
;
⚫
在组长进程的
ID
前面加上一个负号即是操作进程组;
⚫
组长进程不能再创建新的进程组;
⚫
只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关;
⚫
一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离开该进程组;
⚫
默认情况下,新创建的进程会继承父进程的进程组
ID
调用 getpgrp()或 getpgid()可以获取进程对应的进程组 ID
#include <unistd.h>pid_t getpgid(pid_t pid);pid_t getpgrp(void);首先使用该函数需要包含头文件 <unistd.h> 。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid = getpid();
printf("进程组 ID<%d>---getpgrp()\n", getpgrp());
printf("进程组 ID<%d>---getpgid(0)\n", getpgid(0));
printf("进程组 ID<%d>---getpgid(%d)\n", getpgid(pid), pid);
exit(0);
}
调用 setpgid()或 setpgrp()可以加入一个现有的进程组或创建一个新的进程组
#include <unistd.h>int setpgid(pid_t pid, pid_t pgid);int setpgrp(void);使用这些函数同样需要包含头文件 <unistd.h> 。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("更改前进程组 ID<%d>\n", getpgrp());
setpgrp();
printf("更改后进程组 ID<%d>\n", getpgrp());
exit(0);
}
会话
会话是一个或多个进程组的集合,其与进程组、进程之间的关系
一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一 个会话首领(leader
),即创建会话的进程
==================================================================================================================================================
前面学习了进程相关的内容,介绍了如何通过 fork()
或
vfork()
创建子进程,以及在子进程中通过
exec() 函数执行一个新的程序。本章向大家介绍一个新的内容---
进程间通信。
所谓进程间通信指的是系统中两个进程之间的通信,不同的进程都在各自的地址空间中、相互独立、隔离,所以它们是处在于不同的地址空间中,因此相互通信比较难,Linux 内核提供了多种进程间通信的机制。(注意这里是多进程之间的通讯)
⚫
UNIX IPC
:管道、
FIFO
、信号;
⚫
System V IPC
:信号量、消息队列、共享内存;
⚫
POSIX IPC
:信号量、消息队列、共享内存;
⚫
Socket IPC
:基于
Socket
进程间通信。
管道和
FIFO
信号
关于信号相关的内容在本书第八章中给大家介绍过,用于通知接收信号的进程有某种事件发生,所以可用于进程间通信;除了用于进程间通信之外,进程还可以发送信号给进程本身。
消息队列
消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷。消息队列包括 POSIX 消息队列和
System V
消息队列。
消息队列是 UNIX
下不同进程之间实现共享资源的一种机制,
UNIX
允许不同进程将格式化的数据流以消息队列形式发送给任意进程,有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。
信号量
信号量是一个计数器,与其它进程间通信方式不大相同,它主要用于控制多个进程间或一个进程内的多个线程间对共享资源的访问,相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志,除了用于共享资源的访问控制外,还可用于进程同步。
它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源,因此,主要作为进程间以及同一个进程内不同线程之间的同步手段。Linux 提供了一组精心设计的信号量接口来对信号量进行操作,它们声明在头文件 sys/sem.h
中。
共享内存
共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但其它的多个进程都可以访问,使得多个进程可以访问同一块内存空间。共享内存是最快的 IPC 方式,它是针对其它进程间通信方式运行效率低而专门设计的,它往往与其它通信机制,譬如结合信号量来使用,以实现进程间的同步和通信。
套接字(
Socket
)
Socket 是一种
IPC
方法,是基于网络的
IPC
方法,允许位于同一主机(计算机)或使用网络连接起来 的不同主机上的应用程序之间交换数据,说白了就是网络通信,在提高篇章节内容中将会向大家介绍 Linux 系统下的网络编程。
在一个典型的客户端/
服务器场景中,应用程序使用
socket
进行通信的方式如下:
⚫
各个应用程序创建一个
socket
。
socket
是一个允许通信的“设备”,两个应用程序都需要用到它。
⚫
服务器将自己的
socket
绑定到一个众所周知的地址(名称)上使得客户端能够定位到它的位置。