Linux之进程
程序,科学的定义就是编译过的,可执行的二进制代码,这个很好理解。如果程序很大,可以叫做应用,这里提到的程序以及应用都是类似的概念。
进程是指正在运行的程序,一个程序中可以包含多个进程;一个进程可能包含一个或者多个线程。
一、进程ID
1、进程 id 基本概念
- 每一个进程都有一个唯一的标识符,进程 ID 简称 pid。
- 进程的 ID 在一个固定的时刻是唯一的,需要注意的是,假如你在 s 秒的时候有一个进程 ID是 1000,在另外一个时刻 s+n,另一个进程 ID 也有可能是 1000。
- 另外内核运行的第一个进程是 1,也就是内核的 init 程序,这个是唯一的。
- 进程 id 一般默认的最大值为 32768,不过也是可以修改的,当然一般情况下不需要这么做。如果当前进程是1000,那么下一个分配的进程就是 1001,它是严格线性分配的。直到 pid 到了最大值,才重新分配已经用过的进ID,当然这些进程都是已经死亡的进程。
- 除了 init 进程,其它进程都是由其它进程创立的。创立新进程的进程叫父进程,新进程叫子进程。
2、 使用 man 学习 getpid 和 getppid
如下图所示,使用命令“man 2 getpid”。有两个类似的函数 getpid 和 getppid。
接着介绍一下 getpid 和 getppid 的用法。
pid_t getpid(void);
//参数:无。
//返回值:成功返回进程号。
pid_t getppid(void);
//参数:无。
//返回值:成功返回父进程。
3、函数例程
编写简单的 getpid.c 文件测试 getpid 和 getppid 函数。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
void main()
{
pid_t idp,id;
idp = getppid();
printf("ppid = %d\n",idp);
id = getpid();
printf("pid = %d\n",id);
}
4、运行结果
运行程序如下。
如上图所示,pid1209是当前程序的进程号, ppid927是当前进程的父进程。
如何查看当前程序的进程
Linux 中有进程配套的命令 ps 个 kill,可以查看和终止进程。如下图所示,ps 命令查看进程号。
如果是一个循环程序,执行到 while(1),运行的时候可以在执行命令后面添加&,后台运行,然后使用 ps 查询 id 号,使用 kill 命令结束进程。
二、执行新程序-exec 函数族一
在学习创建进程之前,先来学习一下 linux 中重要的 exec 函数族。在 linux 中,exec 函数族是把程序直接载入内存,而不是在一个程序中运行多个进程。
如上图所示,最简单直白的解释就是 exec 函数族调用成功之后,会在内存中执行一个新的程序。在 linux 中要运行多任务需要使用 exec 函数族和 fork 进程。
1、 使用 man 学习 exec 函数
如下图所示,使用命令“man 3 exec”。有六个类似的函数 execl, execlp, execle, execv, execvp, execvpe 。
exec 函数族比较多,但是却很容易记忆和区分的,下面给大家总结一下它们的区别。
如上图所示,首先函数族都是以 exec+xx 的方式命名的。
- “l”和“v”表示参数是以列表还是以数组的方式提供的。
- “p”表示这个函数的第一个参数是*path,就是以绝对路径来提供程序的路径,也可以以当前目录作为目标。
- “e”表示为程序提供新的环境变量。
2、函数例程
编写简单的 exec.c 文件测试 exec 函数。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <unistd.h>
//exec函数族
int main(void)
{
if(execl("/mnt/udisk/helloexec","helloexec","execl",NULL) == -1){
perror("execl error");
exit(1);
}
//程序已经跳转走,如果正常execl不反回错误,下面的代码不会执行!
printf("execl error!\n");
return 0;
}
接着编写简单的 helloexec.c 文件,如下所示。main 函数的第二个参数传递进入之后,argv[1]将被打印出来,如果正常执行则会打印“Hello execl!”。
#include <stdio.h>
int main(int arc,char *argv[])
{
printf("Hello %s!\n",argv[1]);
}
3、运行结果
运行程序如下。
如上图所示,可以看到执行了 helloexec 的打印函数,调用成功。
三、fork 创建新进程
在 linux 中可以使用 fork 创建和当前进程一样的进程,新的进程叫子进程,原来的进程叫父进程。
1、 使用 man 学习 fork 进程
如下图所示,使用命令“man 2 fork”。
接着介绍一下 fork 的用法。
pid_t fork(void);
//参数:无
//返回值:执行成功,子进程 pid 返回给父进程,0 返回给子进程;
//出现错误-1,返回给父进程。执行失败的唯一情况是内存不够或者 id 号用尽,不过这种情况几乎很少发生。
进程函数 fork 的返回值
系统函数 fork 调用成功,会创建一个新的进程,它几乎会调用差不多完全一样的 fork 进程。
子进程的 pid 和父进程不一样,是新分配的。
子进程的 ppid 会设置为父进程的 pid,也就是说子进程和父进程各自的“父进程”不一样。
子进程中的资源统计信息会清零。
挂起的信号会被清除,也不会被继承(后面章节进程通信中会介绍信号)。
所有文件锁也不会被子进程继承。
这里有一个很难理解的地方是,fork 函数的返回值问题。
fork 函数执行成功,子进程 pid 返回给父进程,0 返回给子进程;
出现错误-1,返回给父进程。执行失败的唯一情况是内存不够或者 id 号用尽,不过这种情况几乎很少发生。
2、函数例程
编写简单的 fork.c 文件测试 fork 函数。
#include <stdio.h>
#include <unistd.h>
main()
{
pid_t pid;
int i=100;
pid = fork();
//调用出错
if(pid == -1){
printf("fork failed\n");
return 1;
}
//返回给父进程子进程号,返回值大于0
else if(pid){
i++;
printf("\nThe father i = %d\n",i);
printf("The father return value is %d\n",pid);
printf("The father pid is %d\n",getpid());
printf("The father ppid is %d\n",getppid());
while(1);
}
//返回子进程0,返回值等于0返回给子进程
else{
i++;
printf("\nThe child i = %d\n",i);
printf("The child return value is %d\n",pid);
printf("The child pid is %d\n",getpid());
printf("The child ppid is %d\n",getppid());
while(1);
}
return 0;
}
3、运行结果
运行程序如下。
如上图所示,可以看到打印的父进程和子进程号,然后其中打印的 i 都是一样的。子进程可以使用父进程中定义的变量,和父进程中的变量却是不同的变量。
四、进程终止 exit
在 main 函数的结尾会使用 return 或者exit 结束程序。当使用 exit 的时候,就是使用的进程终止函数 exit。
1、使用 man 学习 exit 函数
如下图所示,使用命令“man 2 exit”。
接着介绍一下 void exit(int status)的用法。
void exit(int status)
//参数:返回给父进程的参数。
//返回值:无。
//函数_exit 和 exit 没什么区别,不需要花太多时间去追究。
//创建进程还有一个 vfork 函数,感兴趣的可以通过网络找个例子实现以下,vfork 的使用会产生很多新问题,使用 vfork 创建的进程结束需要调用_exit。
五、exec 函数族+fork 进程+linux 命令+linux 时间函数例程
1、函数例程
编写简单的 execls.c 文件。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
char *arg[] = {"ls","-a",NULL};
if(fork() == 0){
//in child1
printf("fork1 is OK;execl\n");
if(execl("/bin/ls","ls","-a",NULL) == -1){
perror("execl error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child2
printf("fork2 is OK;execv\n");
if(execv("/bin/ls",arg) == -1){
perror("execv error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child3
printf("fork3 is OK;execlp\n");
if(execlp("ls","ls","-a",NULL) == -1){
perror("execlp error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child4
printf("fork4 is OK;execvp\n");
if(execvp("ls",arg) == -1){
perror("execvp error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child5
printf("fork5 is OK;execle\n");
if(execle("/bin/ls","ls","-a",NULL,NULL) == -1){
perror("execle error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child6
printf("fork6 is OK;execve\n");
if(execve("/bin/ls",arg,NULL) == -1){
perror("execve error");
exit(1);
}
}
//加入小延时可以避免发生混乱的情况
usleep(20000);
return 0;
}
2、运行结果
先把 execls 文件拷贝到当前目录下,使用 ls 命令,查看当前目录,便于和后面执行程序之后对比。
运行后部分结果如下图所示。
如上图所示,可以和前面执行 ls 命令之后对比。