目录
六、进程管理
1.程序和进程
- 程序
- 程序(program)是存放在磁盘文件中的可执行文件。
- 进程和进程ID
- 程序的执行实例被称为进程(program)。
- 每个linux进程都一定有一个唯一的数字标识符,称为进程ID(process ID)。进程ID总是一非负整数。
2、linux下的进程结构
- linux系统时一个多进程的系统,进程之间具有并行性、互不干扰的特点。
- linux中进程包含PCB(进程控制块)、程序以及程序锁操纵的数据结构集,可分为代码段、数据段、堆栈段。
3、进程状态
参数 | 描述 |
---|---|
R | 运行状态(TASK_RUNNING) |
S | 可中断睡眠状态(TASK_INTERRUPTIBLE) |
D | 不可中断睡眠状态(TASK_UNINTERRUPTIBLE) |
T | 暂停状态(TASK_STOPPED或TASK_TRACED) |
Z | 僵死状态(TASK_ZOMBLE) |
X | 退出状态(TASK_DEAD) |
4、进程状态转换图
5、init进程
- 进程ID为1通常是init进程,在自举过程结束时由内核调用
- init进程绝不会终止
- 它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。
6、获取进程标识
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); //返回调用进程的进程ID
pid_t getppid(void); //返回:调用进程的父进程I D
uid_t getuid(void); //返回:调用进程的实际用户I D
uid_t geteuid(void); //返回:调用进程的有效用户I D
gid_t getgid(void); //返回:调用进程的实际组I D
gid_t getegid(void); //返回:调用进程的有效组I D
7、fork系统调用
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
//返回:子进程中为0,父进程中为子进程ID,出错-1
-
由fork创建的新进程被称为子进程(child process)。
-
该函数被调用一次但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是子进程的进程ID。
-
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。
-
使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:
- 进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。
-
子进程与父进程的区别在于:
- 父进程设置的锁,子进程不继承
- 各自的进程ID和父进程ID不同
- 子进程的未决告警被清除
- 子进程的未决信号集设置为空集
fork 系统调用注意点
- fork系统调用之后,父子进程将交替执行。
- 如果父进程先退出,子进程还没退出那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)
- 如果子进程先退出,父进程还没有退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵进程。
8、替换一个进程映像(exec)
当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈断。
-
功能:用exec函数可以把当前进程替换为一个新进程。exec名下是由多个关联函数组成的一个完整系列
-
原型
#include <unistd.h> int execl(const char *path,const char *arg,...);//使用参数列表 int execlp(const char *path,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[]);//通过指针数组的方式来传递参数
-
参数
- path:表示你要启动程序的名称包括路径名
- arg:表示启动程序所带的参数
-
返回值
- 成功返回0,失败返回 -1
示例
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Runnning ps with execlp\n");
execl("bin/ls","ls","-l","/etc/passwd",(char *)0);
execlp("ls","ls","-l","/etc/passwd",(char *)0);// ls -l /etc/passwd
printf("Done.\n");
exit(0);
}
9、启动新的进程(system)
- 功能:可以让一个程序在另一个程序的内部运行,也就是说,我们创建了一个新的进程。这个工作可以通过库函数system来实现。
- 原型:
#include <stdlib.h>
int system(const char *string);
- 参数
- string:你要启动程序的名称
- 返回值
- 如果无法启动shell运行命令,system将返回 “127”;
- 出现不能执行system调用的其他错误时返回 “-1”。
- 如果system能够顺利执行,返回那个命令的退出码。
示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("PID:%d\n",getpid());
printf("Running ps with system\n");
system("ps -ef");
printf("Done\n");
exit(0);
}
-
system函数远非是启动其他进程的理想手段,因为它必须用一个shell来启动预定的程序。
-
对shell的安装情况和它所处的环境的依赖也很大
-
效率很低
10、wait和waipid函数
什么是僵尸进程
- 当一个子进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行或者父进程调用了wait才告终止。
- 进程表中代表子进程的数据项是不会立刻释放的,虽然不再活跃了,可子进程还停留在系统里,因为它的退出码还需要保存起来以备父进程中后续的wait调用使用。它将称为一个“僵进程”。
wait和waipid函数
-
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。
-
父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号的系统默认动作是忽略它。
-
wait函数用于使父进程阻塞,直到一个子进程结束或者该进程接收到一个指定信号为止。
wait函数返回说明
调用wait或waitpid的进程可能会:
- 阻塞(如果其所有子进程都还在运行)。
- 带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态)。
- 出错立即返回(如果它没有任何子进程)。
wait函数
- 功能:使父进程阻塞,直到一个子进程结束或者该进程接收到了一个指定的信号为止。如果该父进程没有子进程或者它的子进程已经结束,则wait()函数就会立即返回。
wait返回状态检测
宏定义 | 描述 |
---|---|
WIFEXITED(stat_val) | 如果子进程正常结束,返回一个非零值 |
WEXITSTATUS(stat_val) | 如果WIFEXITED非零,返回子进程退出码 |
WIFSIGNALED(stat_val) | 子进程因未捕获信号而终止,返回非零值 |
WTERMSIG(stat_val) | 如果WIFSIGNALED非零,返回信号代码 |
WIFSTOPPED(stat_val) | 如果子进程终止,返回一个非零值 |
WSTOPSIG(stat_val) | 如果WIFSTOPPED非零,返回一个信号代码 |
waitpid函数
- 功能:waitpid()的作用和wait()一样,但它并不一定要等待第一个终止的子进程(它可以指定需要等待终止的子进程),它还有若干选项,可以不阻塞父进程。
wait和waitpid的区别
- 在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项,可使调用者不阻塞。
- waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的特定进程。
- 实际上wait函数是waitpid函数的一个特例。
11、exit和_exit
- exit和_exit用于中止进程;
- _exit的作用:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的数据结构;
- exit与_exit函数不同,exit函数在调用exit系统之前要检查文件打开情况把文件缓冲区的内容写回文件中去。如调用printf()函数。
‘\n’ 会立刻把文件缓冲区的内容写回文件中去。所以在测试exit和_exit的区别时,在printf不要加’\n’。
exit工作原理图
12、守护进程
什么是守护进程?
- 守护进程是在后台运行不受终端控制的进程
- 守护进程能自动转到后台并且脱离于终端的联系
- Linux系统中一般有很多守护进程在后台运行,执行不同的管理任务
守护进程的特征
- 最重要特征是后台运行
- 守护进程必须与运行前的环境隔离开来
- 守护进程的启动方式有其特殊之处
守护进程运行环境
- 包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩码等。
- 这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。
守护进程启动方法
- 在系统启动时很多守护进程都是由系统初始化脚本启动。
- 这些脚本一般在/etc目录或以/etc/rc开头的目录下,它们的位置和内容依赖于具体的实现
守护进程启动示例
- 许多网络服务器是由inetd超级服务器启动的,inetd是由系统初始化脚本启动的
- cron守护进程按规则定期执行一些程序,由它启动的程序也以守护进程的方式运行,cron是由系统初始化脚本启动
- 不管是在前台还是在后台,守护进程也可以在用户终端上启动
守护进程消息处理
- 由于守护进程没有控制终端,在发生问题时它要用一些其他方式以输出消息。这些消息既有一般的通告消息,也有需要管理员处理的紧急事件消息。
- syslog函数是输出这些消息的标准方式,它将消息发往syslog守护进程
进程和进程组
- 进程属于一个进程组
- 进程组号(GID)就是进程组长的进程号(PID)
- 进程组通常是从父进程继承过来
- 登陆会话可以包含多个进程组
- 所有的进程组共享一个控制终端
守护进程建立流程
后台运行子进程
-
避免挂起,控制终端将守护进程放入后台执行。
-
方法是在进程中调用fork使父进程终止,
if(pid=fork())
exit(0);//是父进程,结束父进程,子进程继续
脱离运行环境
- 控制终端,登录会话和进程组通常是从父进程继承下来的
- 我们的目的就是要摆脱它们,使之不受它们的影响。
- 方法是在第1点的基础上,调用setsid()使进程成为会话组长。
setsid系统调用
- 创建守护进程最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader。
- 该函数调用成功时返回新创建的Session的id(其实也就是当前进程的id),出错返回-1。注意,调用这个函数之前,当前进程不允许是进程组的Leader,否则该函数返回-1。要保证当前进程不是进程组的Leader也很容易,只要先fork再调用setsid就行了。fork创建的子进程和父进程在同一个进程组中,进程组的Leader必然是该组的第一个进程,所以子进程不可能是该组的第一个进程,在子进程中调用setsid就不会有问题了。
禁止进程重新打开控制终端
- 守护进程虽然已经称为无终端的会话组长,但是它可能重新申请打开一个新的控制终端
- 如果这个守护进程不是会话组长,它就不能打开新的控制终端。所以我们可以先结束第一个子进程,再创建第二个子进程,这样第二个子进程就不会是进程组长了
关闭打开的文件描述符
-
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。
-
可按照如下格式关闭文件描述符
for(i=0;i<打开的文件描述符数;i++)
close(i);
改变当前目录工作目录
- 进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmp
- 使用函数 chdir(const char*);来更改工作目录
重设文件创建掩码
- 进程从创建它的父进程那里继承了文件创建掩码
- 可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩码清除
- 使用函数 umask(0);清除文件创建掩码
忽略SIGHUP信号
- 当会话头(第一次生成的子进程)终止时,该会话中的所有进程(第二次生成的子进程)都会收到SIGHUP信号
- 使用 signal(SIGHUP,SIG_IGN); 来忽略掉这个信号
守护进程示例
#include <unistd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void init_daemon(void)
{
int pid;
int i;
if(pid = fork())
exit(0);//是父进程,结束父进程
else if(pid < 0)
exit(1);//fork失败,退出
//是第一子进程,后台继续执行
setsid();//第一子进程成为新的会话组长和进程组长
//并与控制终端分离
if(pid = fork())
exit(0);//是第一子进程,结束第一子进程
else if(pid < 0)
exit(1);//fork失败,退出
//是第二子进程,继续
//第二子进程不再是会话组长
for(i = 0;i < NOFILE;i++)
close(i);
chdir("/tmp");//改变工作目录到/tmp
umask(0);//重设文件创建掩码
return;
}
int main()
{
FILE *fp;
init_daemon();//初始化为Daemon
while(1)//每隔2s向test.log报告运行状态
{
sleep(2);
if(fp = fopen("test.log","a"))
{
time_t t = time(NULL);
fprintf(fp,"I'm here at %s\n",asctime(localtime(&t)));
fclose(fp);
}
}
return 0;
}
如果我的文章能够帮到您,可以点个赞!
您的每次 点赞、关注、收藏 都是对我最大的鼓励!