一、进程概念
进程是指正在运行的程序,一个程序中可以包含多个进程;一个进程可能包含一个或者多
个线程。
1.1 进程ID
每个进程都有一个唯一的标识符,叫做进程ID,简称pid。
内核运行的第一个进程是init
程序,pid为1,是唯一的。
除了init
进程,其他进程都有由别的进程进行创建的。创建新进程的进程叫父进程,创建的新进程叫做子进程。
1.2 获取进程
在系统调用函数中,getpid
和getppid
函数均可以用来获取进程的ID号。
需要包含的头文件为:#include<sys/types.h>
和#include<unistd.h>
。
函数原型分别为:
pid_t getpid(void);
pid_t getppid(void);
根据描述,getpid
返回进程的子进程,getppid
返回进程的父进程。
简单获取进程:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t ppid, spid;
spid = getpid();//获取子进程
if(spid)
printf("spid = %d\n", spid);
ppid = getppid();//获取父进程
if(ppid)
printf("ppid = %d\n", ppid);
}
进程如下:
1.3 查看进程、杀死进程
在终端输入top
可以实时查看当前的进程运行情况,ps
可以查看当前时间的进程情况。
使用kill+ID
可以杀死某个进程。
二、创建进程
当需要在程序中进行创建新的程序时,可以使用exec
族函数和fork
进程调用函数。使用exec方式会比较直接,fork方式则比较复杂。
2.1 exec方式
使用exec
函数族需要注意,当某个进程调用了exec
函数族去执行一个新的程序或命令时,这个进程会在执行exec
完后结束自己,不会往下执行与父进程相同的内容。
exec
族函数属于C库函数,查看man手册可知,包含六个函数:execl
, execlp
, execle
, execv
, execvp
, execvpe
,需要包含的头文件为#include <unistd.h>
。
这几个函数可以简单进行区分:
-
“l”
和“v”
表示参数是以列表还是以数组的方式提供的。它们的区别在于,execv 开头的函数是以“char *argv[]”(vector)形式传递命令行参数的,而execl 开头的函数采用罗列(list)的方式,把参数一个一个列出来,然后以一个NULL表示结束 -
“p”
表示这个函数的第一个参数是path,就是以绝对路径来提供程序的路径,也可以以当前目录作为目标。 -
“e”
表示为程序提供新的环境变量。
2.1.1 execl函数:
main.c:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if(execl("test/hello", "hello", "First!", NULL) == -1)
{
printf("execl hello failed!\n");
return -1;
}
printf("can not excel hello!\n");
return 0;
}
hello.c:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("arg[1] = %s\n", argv[1]);
printf("hehehe\n");
}
输出:
2.1.2 excev函数:
main.c修改为:
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char *Eargv[] = {"hello", "First!", NULL};
if(execv("test/hello", Eargv) == -1)
{
printf("execl hello failed!\n");
return -1;
}
printf("can not excel hello!\n");
return 0;
}
hello.c内容不变,编译执行:
输出结果一致。
区别:
从上面的结果可以看出,execl
和execv
的区别在于调用程序的参数传递方式不同。带l
的函数通过列表的方式直接在本函数参数中传递,遇到NULL
即参数结束;带v
的函数则是通过一个字符指针数组封装参数后直接传递给函数。
2.2 fork方式
在 linux 中可以使用 fork 创建和当前进程一样的进程,新的进程叫子进程,原来的进程叫父进程。
函数的原型只有一个:
pid_t fork(void);
返回值:执行成功,子进程 pid 返回给父进程,0 返回给子进程;出现错误-1,返回给父
进程。
内存不够或者 id 号用尽时,会出现错误。
参考一些经典的例子,首先理解一下的fork的流程:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char **argv)
{
pid_t fpid;
int count = 0;
fpid = fork();
if(fpid < 0)
printf("fork err!\n");
else if (fpid == 0)
{
printf("I am child process, ppid pid fpid: %4d %4d %4d\n", getppid(), getpid(), fpid);
count++;
}else{
printf("I am parent process, ppid pid fpid: %4d %4d %4d\n", getppid(), getpid(), fpid);
count++;
}
printf("final count = %d\n", count);
return 0;
}
执行结果:
分析:
fork
执行成功后,创建了一个子进程,子进程的ID为2246,返回给当前进程2245(父进程),644则是2245即当前进程的父进程。
创建的子进程2246跟2245一样,会执行fork下面程序。也就是说在语句fpid=fork()
之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)
。
执行子进程2246时,父进程自然是2245,根据fork的描述,fork
执行后返回给子进程的是0,因此fpid = 0;
。
可以用下面这幅图描述这个过程:
再通过一个复杂一点的例子进行理解:
/*file: test.c*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char **argv)
{
int i = 0;
printf("i son/par ppid pid fpid\n");
for(i = 0; i < 2; i++){
pid_t fpid = fork();
if(fpid == 0)
printf("%d child %4d %4d %4d\n", i, getppid(), getpid(), fpid);
else
printf("%d parent %4d %4d %4d\n", i, getppid(), getpid(), fpid);
}
return 0;
}
执行结果:
分析:
i=0第一次循环,当前父进程是2262,fork创建了一个子进程2263, 664为当前进程的父进程,用链表表示:
644->2262->2263(第二行)
创建的子进程2263中,fork返回给2263的是0,所以子进程的子进程ID=0,链表表示:
2262->2263->0(第三行)
接着,在之前的进程2262中,i=1,fork执行,再次创建一个子进程2264,父进程仍为644,链表表示:
664->2262->2264(第四行)
然后创建的子进程2264也会执行后面的判断,fork给这个子进程返回的ID也是0,所以:
2262->2264->0(第五行)
最后由2262创建的第一个子进程2263也会做相同的操作,这个进程的i++,i=1,执行fork创建子进程2265,所以:
2262->2263->2265(第六行)
接着2263创建的子进程2265也会做相同的事情,fork给它返回0,所以:
2263->2265->0(最后一行)
此时这些进程的i
均为1,再次执行会完成当前进程退出,如果前面的某些进程已经退出被杀死了,则由这些被杀死的进程创建的子进程的父进程id就会变为1,如下:
进程执行的先后和什么时间杀死进程并不一定都一样。大致过程描述如下:
每次fork都会创建新的子进程,直到条件不满足。这样就进行了三次fork,并且创建了三个子进程。
三、终止进程
前面讲了如何获取、创建进程,只剩下终止或者叫杀死进程了。终止一个进程使用exit
函数。需要包含标准库头文件stdlib.h
。
原型:
void exit(int status);
参数status
是传递给父进程的参数。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
char *Myargv[] = {"ls", "-l", NULL};
printf("start ppid pid = %d, %d\n", getppid(), getpid());
if(fork() == 0)
{
printf("fork 1 success!\n");
printf("ppid pid = %d, %d\n", getppid(), getpid());
if(execv("/bin/ls", Myargv) < 0)
{
printf("execv failed!\n");
exit(1);//退出进程
}
printf("end ppid pid = %d, %d\n", getppid(), getpid());//无法执行,exec族函数会结束子进程
}
sleep(2);//延时两秒
if(fork() == 0)//如果当前为子进程
{
printf("fork 2 success!\n");
printf("ppid pid = %d, %d\n", getppid(), getpid());
if(execl("/bin/ls", "ls", "-lh", NULL) < 0)
{
printf("execl failed!\n");
exit(1);
}
printf("end ppid pid = %d, %d\n", getppid(), getpid());//无法执行,exec族函数会结束子进程
}
sleep(2);//延时两秒
if(fork() == 0)//如果当前为子进程
{
printf("fork 3 success!\n");
printf("ppid pid = %d, %d\n", getppid(), getpid());
if(execlp("ls", "ls", "-la", NULL) < 0)
{
printf("execlp failed!\n");
exit(1);
}
printf("end ppid pid = %d, %d\n", getppid(), getpid());//无法执行,exec族函数会结束子进程
}
sleep(1);
printf("final ppid pid = %d, %d\n", getppid(), getpid());
printf("excute excelp ls -la----------------\n");
execlp("ls", "ls", "-la", NULL);
sleep(1);
printf("finish!\n");
return 0;
}
结果:
当前进程916,第一次fork创建一个子进程917,fork返回给子进程917的值为0,所以执行第一个if(fork() == 0)
,917执行到excev
函数之后,就会结束当前的917进程(exce
族函数执行后会结束当前进程),所以不会917不会有后续fork行为。
916进程这边会继续fork,创建子进程924,924进程执行execl
函数后也会结束自己。
最后916进程执行最终的fork,创建子进程925,925进程同样执行execlp
函数后结束自己。
这样最后查看PID,只有原来的916进程在执行,916在rerurn
之前调用了exec
族函数,也会结束自己。因此看不到finish。
过程示意图: