Linux C应用编程--进程

1 进程概念

进程是一个可执行程序的实例,进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后,它就称为了一个进程, 一个进程的生命周期便是从创建开始直至其运行结束

1.1进程号

每一个进程都有一个进程号( processID ,简称 PID),用于唯一标 识系统中的某一个进程。在Ubuntu系统 shell 终端下通过 ps 命令 可以查到系统中进程相关的一些信息,包括每个进程 的进程号。
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 进程的环境变量

新的进程在创建之前,会继承其父进程的环境变量副本。
每一个进程都有一组与其相关的环境变量,这些环境变量以字符串形式存储在一个字符串数组列表中,把这个数组称为环境列表。其中每个字符串都是以“名称 = 值( name=value )”形式存在。
// 在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() 区别

  1. 调用 fork () 之后,将产生竞争条件,无法确定父、子两个进程谁将率先访问 CPU;如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它。
  2. vfork() 保证子进程先运行,子进程调用 exec 之后父进程才可能被调度运行。
  3. 当子进程并不需要用到父进程的数据段、堆段、栈段(譬如父程序中定义的局部变量、全局变量等)中的数据时,vfork()系统调用在效率上要优于 fork()
  4. 虽然 vfork()系统调用在效率上要优于 fork(),但是 vfork()可能会导致一些难以察觉的程序 bug,所以尽量避免使用 vfork()来创建子进程。

3 监视子进程

3.1 wait() 函数

系统调用 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()函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子进程的一些资源,俗称为子进程“收尸”。
参数 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

限制:

  • 如果父进程创建了多个子进程,使用 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)等价。

statuswait()函数的 status 参数意义相同。

options:参数 options 是一个位掩码,可以包括 0 个或多个如下标志:
  • WNOHANG如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
  • WUNTRACED除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进 程状态信息;
  • WCONTINUED返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。

返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0

3.3 waitid() 函数

waitid() waitpid() 类似,不过 waitid() 提供了更多的扩展功能

4 僵尸进程与孤儿进程

4.1 孤儿进程

  1. 父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程。
  2. Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程

 4.2 僵尸进程

如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。
  • 当父进程调用 wait()(或其变体)为子进程“收尸”后,僵尸进程就会被内核彻底删除。
  • 如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(),故而从系统中移除僵尸进程。
  • 僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死。只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!

4.3 及时回收僵尸进程:SIGCHLD 信号

子进程的终止属于异步事件,父进程事先是无法预知的,如果父进程有自己需要做的事情,它不能一直 wait()阻塞等待子进程终止(或轮训),这样父进程将啥事也做不了,因此可以使 SIGCHLD 信号回收僵尸进程。
父进程收到 SIGCHLD 信号的方式:
  1. 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
  2. 当父进程的某个子进程因收到信号而发生状态改变时(终止、暂停或恢复),父进程会收到 SIGCHLD 信号。

SIGCHLD 信号处理函数中循环以非阻塞方式来调用 waitpid() ,直至再无其它终止的
子进程需要处理为止,所以,通常 SIGCHLD 信号处理函数内部代码如下所示:
while (waitpid(-1, NULL, WNOHANG) > 0)  // 代码一直循环下去,直至 waitpid()返回 0,表明再无僵尸进程存在;或者返回-1,表明有错误发生。
continue;
应在创建任何子进程之前,为 SIGCHLD 信号绑定处理函数。

 5 在旧程序代码中执行另外一个编写的新程序代码(一般用于编写子进程)

5.1 execve() 函数

系统调用 execve() 可以将新程序加载到某一进程的内存空间,通过调用 execve() 函数将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行。将 execve() 函数放在子进程中即可为子进程单独编写程序文件。
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

2. 如果 参数 command 不为 NULL:
  • 如果无法创建子进程或无法获取子进程的终止状态,那么 system()返回-1
  • 如果子进程不能执行 shell,则 system()的返回值就好像是子进程通过调用_exit(127)终止了
  • 如果所有的系统调用都成功,system()函数会返回执行 command shell 进程的终止状态。
system() 函数其内部的是通过调用 fork() execl() 以及 waitpid() 这三个函数来实现它的功能,首先 system() 会调用 fork() 创建一个子进程来运行 shell (可以把这个子进程成为 shell 进程),并通过 shell 执行参数 command 所指定的命令。譬如:
#include <stdlib.h>

system("ls -la")
system("echo HelloWorld")

7 进程状态

Linux 系统下进程通常存在 6 种不同的状态,分为:
  1. 就绪态(Ready):指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到CPU 就能够直接运行;
  2. 运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态;
  3. 僵尸态:僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”;
  4. 可中断睡眠状态(浅度睡眠):表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒;
  5. 不可中断睡眠状态(深度睡眠):深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。
  6. 暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 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 方式,它是针对其它进程间 通信方式运行效率低而专门设计的,它往往与其它通信机制,譬如结合信号量来使用,以实现进程间的同步和通信。

进程间通信1

进程间通信2

8.6 套接字(Socket) 

Socket 是一种基于网络的 IPC 方法,允许位于同一主机(计算机)或使用网络连接起来 的不同主机上的应用程序之间交换数据,说白了就是网络通信。

在典型的客户端/服务器场景中,应用程序使用 socket 进行通信的方式如下:

  •  各个应用程序创建一个 socket。socket 是一个允许通信的“设备”,两个应用程序都需要用到它。
  •  服务器将自己的 socket 绑定到一个众所周知的地址(名称)上使得客户端能够定位到它的位置。 

套接字1 

套接字2

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值