本篇是Linux系统编程系列的第二篇,介绍进程相关概念,进程控制、exec函数族、守护进程。本篇学习重点的在于理解进程相关的概念,熟练使用进程控制函数,熟悉守护进程创建的步骤。
多任务是指同一时刻多个任务同时运行。我们的手机可以在聊微信的同时使用网易云播放音乐就是操作系统多任务的体现。“同一时刻”实际上并非真正的同一时刻,单核的处理器在同一时刻只能运行一个任务。每个任务创建时,操作系统就会分配一定量的时间片(ms级)给它,当运行到这个时间片时,就会调度这个任务运行,时间片结束后,操作系统又会调度其它任务运行,以此往复。处理器的速度远远快于我们人眼的反应速度,因此,多任务机制是宏观并行,微观串行。
进程是一个程序执行的过程,是操作系统动态执行的基本单元和资源分配的基本单元。
程序是一个抽象的静态概念,是指令在磁盘上有序的集合。
进程是一个抽象的动态概念,包括动态创建、调度和消亡的整个过程。是程序执行的一个过程。
举个不一定恰当的例子:如果程序是软件流程图,告诉我们如何去编写代码。那么进程就是编写代码的具体过程,包含创建文件、添加头文件、复制代码、拷贝代码等等。
每个进程都有唯一的进程号(PID),除了自身的进程号,每个进程还有父进程号(PPID)。在Linux 中,可以使用 getpid()和 getppid()这两个系统调用函数获得当前进程的PID和PPID。
根据进程的运行状态,可以分成三种形态。
执行态:该进程正在运行,即进程正在占用 CPU。
就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间片。
等待态:进程不能使用 CPU,若等待事件发生(等待的资源分配到)则可将其唤醒。
在Linux中进程可以分为以下三种进程:
交互进程:在shell下启动,在前台运行也可以在后台运行。
守护进程:和终端无关,一直在后台运行,生命期很长。
批处理进程:这种进程和终端没有联系,是一个进程序列。
系统编程专栏主要侧重点在系统编程的知识点,因此命令简单带过,嵌入式需要掌握的常用命令会在Linux基础专栏中介绍。
ps:查看进程快照。
ps -ef:查看系统中所有进程的简要信息。
ps -ef | grep test:根据关键字显示进程信息。
ps -aux:和ps -ef的输出结果差别不大,但展示风格不同。
top:动态显示进程信息。
/proc:查看进程详细信息。
nice -n nice值 ./test:按用户指定的优先级运行进程。nice值范围:-20~19。值越小优先级越高,普通用户最小指定nice值为0。
renice -n nice值 进程号:改变已有进程的优先级。普通用户只能增加nice值。
jobs:查看后台进程。
fg 作业号:把后台进程变成前台进程。
bg 作业号:将挂起的进程运行起来。
32位处理器在Linux系统下会给每个进程分配4G的虚拟内存,其中前3G是用户空间内存,最后1G是内核空间内存。有同学可能会疑惑了,我们的物理内存都不到4G,每个进程是怎么做到各自都有4G的虚拟内存呢?这就是涉及到虚拟内存在物理内存上映射机制的问题了,由于内容较多,将在专题中介绍。
本专栏着重对应用开发的介绍,因此以下介绍的是用户空间。用户空间从低地址到高地址由代码段(text)、数据段(data、bss)、堆(heap)、映射段(mmap)、栈(stack)组成。
名称 | 存储内容 |
---|---|
代码段 | 存放程序代码的数据 |
data 数据段 | 已初始化且初值非0的全局变量和静态局部变量 |
bss 数据段 | 未初始化或初值为0的全局变量和静态局部变量 |
堆 | malloc动态分配的内存 |
映射段 | 直接映射了磁盘文件的内容 |
栈 | 局部变量、函数参数、返回地址等 |
进程控制块位于进程的内核空间内存中,每个进程都自己独有的进程控制块,记录了进程的相关信息,相当于进程的身份证。进程控制块实际上就是一个在内核空间的是task_struct结构体,它主要由以下内容:
程序运行时会创建一个进程,同时一个新的进程组也被创建,每个进程属于一个进程组,进程创建的子进程和父进程同属一个进程组,简单理解为进程组和程序是一一对应的。
会话是一个或多个进程组的集合,通常用户打开一个终端时,系统会创建一个会话。所有通过该终端打开的进程都属于这个会话,这个终端被称为控制终端。一个会话只能打开一个终端。终端关闭时,所有相关进程会关闭,因此守护进程要独立于终端。
fork是创建一个进程的唯一方法,与我们理解的普通函数不同的是,为了区分父子进程,进程创建成功后,fork有两个返回值。其中在父进程中返回子进程的进程号,在子进程中返回0。进程创建出错返回-1。
从已存在的进程中创建一个新的进程,这个新的进程称为子进程。子进程复制父进程的0到3g空间和父进程内核空间中的PCB,而子进程所独有的只有它的进程号、资源使用和计时器等,由此可见创建一个子进程的开销会很大。子进程结束后除PCB外的资源都被释放,父进程应及时回收,若不及时回收,子进程会变成僵尸进程,等到父进程结束后才会被init进程回收,init进程是Linux启动后创建的第一个进程。若父进程先结束,子进程会变成孤儿进程,被init进程收养,此时的子进程会变成后台进程。由于子进程复制了父进程的PC寄存器,因此子进程是从fork后的下一条指令开始执行。至于子进程和父进程谁先执行的问题,答案是都有可能,具体看系统先执行父进程的时间片还是子进程时间片。
函数原型
pid_t fork(void)
返回值
0:子进程,子进程号(正整数),-1:出错。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
pid_t pid;
/* 调用fork创建子进程 */
pid = fork();
if(pid < 0)
{
printf("fork error\n");//fork出错
}
else if(pid == 0)//子进程,else if(pid == 0)内执行的是是子进程了
{
printf("this is child process!my pid is %d\n",getpid());//前文提到getpid可获得进程号
}
else//fork的返回值只有三种可能,前两种可能在前面已经列出,因此else内执行的是父进程了
{
printf("this is father process!my pid is %d\n",getpid());
}
return 0;
}
前文提到过调用fork函数后,子进程几乎复制了父进程的所有内容,代码段也包含其中。为了让子进程执行不同的程序,可以调用exec函数族来启动另一个程序,从而替换子进程的代码段、数据段、堆栈等内容。exec启动的可执行文件可以是二进制文件,也可以是Linux下的任何可执行文件,如shell脚本等。
函数原型
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
传入参数
path:包含文件名在内的完整路径。
file:可以只给出文件名,系统会自动按照环境变量"$PATH"指定的路径区查找。
argv[]:字符数组形式的命令行参数。数组最后一个成员必须是NULL。
arg:可执行文件的命令行参数,可以是多个,此时跟在arg后的参数必须传入NULL。
envp[]:指定新进程的环境变量。数组最后一个成员必须是NULL。
返回值
出错返回-1。
例子
传入参数可能看得云里雾里,用以下几个例子较清晰:
char *const ps_argv[] ={
"ps", "-ef", NULL};
char *const ps_envp[] ={
"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
execv("/bin/ps", ps_argv);
execle("/bin/ps", "ps", "-ef", NULL, ps_envp);
execve("/bin/ps", ps_argv, ps_envp);
execlp("ps", "ps", "-ef", NULL);
execvp("ps", ps_argv);
以上函数的使用结果都是一样,相当于在命令行输入ps -ef,即查看当前所有进程的意思。
记忆方法
函数名可以区分这些函数:
实际上,只有execve是真正的系统调用,其它五个函数最终都调用execve。
进程的中止有5种方式:main自然返回;调用exit函数;调用_exit函数;接收到终止信号,如终端输入Ctrl+C;调用abort函数产生SIGABRT信号。前3种是正常的进程终止方式,后2种是异常终止。下面我们介绍exit函数和_exit函数终止进程。
exit函数和_exit函数都可以终止进程,区别在于_exit函数会无条件停止进程所有操作,清除进程相关内容,终止进程,而exit函数会先检查文件打开情况,将文件缓冲区的内容写入文件后再终止进程。为了保证数据完整性,常用exit函数来终止进程。
函数原型
void _exit(int status)
void exit(int status)
传入参数
status:整型参数,通过该参数可以传递进程结束时的状态,一般用0表示进程正常结束。可以用wait函数接收子进程的返回值。
前文提过,子进程结束后父进程应及时回收。可以调用wait函数或waitpid函数来回收子进程。
调用wait函数后父进程阻塞等待,直到有子进程结束或父进程被其它信号中断,若该进程没有子进程或者子进程在调用wait函数前已经结束,wait函数会立即返回。
waitpid比wait更加灵活,wait回收的是调用函数后最先结束的子进程,waitpid则有更多的选项。此外,waitpid可以选择非阻塞方式。
函数原型
pid_t wait(int *status)
传入参数
status:表示子进程结束时的状态。可以用WIFEXITED(status)和WEXITSTATUS(status)两个宏来获取进程结束的返回值,若WIFEXITED(status)返回非0,则进程是正常结束,当WIFEXITED(status)返回非0时,可以用WEXITSTATUS(status)提取子进程的返回值,即exit和_exit的status值或return的返回值。
返回值
成功返回回收的子进程号,失败返回-1。
函数原型
pid_t waitpid(pid_t pid, int *status, int options)
传入参数
pid:>0:回收指定进程号的子进程,-1:回收任意子进程,0:回收和当前调用waitpid一个组的所有子进程,<-1:回收指定进程组内的任意子进程,该组ID为pid的绝对值
status:同wait。
options:选项。
options | 含义 |
---|---|
WNOHANG | 即使没有子进程结束, waitpid()也不阻塞,会直接返回,此时返回值为 0 |
WUNTRACED | 如果子进程进入暂停执行情况则马上返回, 但结束状态不予以理会,通常在调试时使用 |
0 | 阻塞等待子进程退出,此时同wait |
返回值
成功返回回收的子进程号,失败返回-1,使用选项 WNOHANG 且没有子进程退出返回0。
守护进程是Linux三种进程类型之一,通常在系统启动时运行,系统关闭时结束,很多服务程序以守护进程方式运行。与依附于终端的进程不同的是守护进程始终在后台运行,独立于任何终端,周期性执行某种任务或等待特定事件,不会因终端关闭而关闭。
创建子进程,父进程退出后子进程变成孤儿进程,被init进程收养。此时子进程在后台运行,依旧依附于终端。
在子进程种调用setsid创建新会话,此时子进程成为新的会话组长,脱离原先的终端。
守护进程一直在后台运行,其工作目录不能被卸载,因此工作目录需要指向一个永远不会被卸载的目录。当前工作目录可以调用chdir函数更改为/或者/tmp,其中/可读可执行但不可写,/tmp可读可写可执行。
在IO编程种提到过文件掩码会屏蔽掉文件权限种的对应位。使用fork函数创建的子进程复制了父进程几乎所有的内容,同样,文件权限掩码也被复制了,为了增加子进程的灵活性,可以调用umask函数把子进程的文件权限掩码设置为0。
我们知道一个进程创建后会自动生成标准输入、标准输出、标准错误3个文件描述符,而守护进程独立于终端运行,这3个文件已经失去了存在的意义,因此为了减少系统开销需要关闭这3个文件。同样,其它文件从父进程继承过来的文件也需要关闭,调用getdtablesize函数可以获得文件个数,从而使用循环语句关闭依次这些文件。至此守护进程创建完成。
创建一个守护进程,每10秒会在指定文件种输入内容。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
int i, fd;
char *buf = "This is a Daemon\n";
/* 第一步:创建子进程,父进程退出 */
pid = fork();
if (pid < 0)
{
printf("Error fork\n");
exit(1);
}
else if (pid > 0)
{
exit(0); /* 父进程退出 */
}
/*第二步:更改当前工作目录*/
setsid();
/*第三步:更改当前工作目录*/
chdir("/");
/*第四步:重设文件权限掩码*/
umask(0);
/*第五步:关闭打开的文件描述符*/
for(i = 0; i < getdtablesize(); i++)
{
close(i);
}
/*这时创建完守护进程,以下开始正式进入守护进程工作*/
while(1)
{
if ((fd = open("/tmp/daemon.log",O_CREAT|O_WRONLY|O_APPEND, 0600)) < 0)
{
printf("Open file error\n");
exit(1);
}
write(fd, buf, strlen(buf) + 1);
close(fd);
sleep(10);
}
exit(0);
}