【Linux篇】--进程
这里写目录标题
- 一、理解进程
- 二、进程的创建
- 三、进程的运行
- exec系列
- 四、进程的控制操作
- 终止
- 进程的同步
- 五、进程属性
- 进程标识符
- 进程的组表示符
- 进程环境
- 六、守护进程
- 概念
- 启动
- 错误输出
- 守护进程的建立
一、理解进程
在 UNIX 中,进程是正在执行的程序。它相当于 Windows 环境内的任务这一概念。每
个进程包括程序代码和数据。其中数据包含程序变量数据、外部数据和程序堆栈等。
系统的命令解释程序shell专门建立一个进程来执行cat命令,就要新建立一个进程并运行它,如:
cat filel
该命令就会使shell新建立个进程来运行cat命令。
例2:
ls|wc -ll
这个命令就会使shell建立两个进程,以并发运行命令ls和wc,把目录列表命令 ls 的输
出通过管道送至字计数命令 wc。
常见命令操作:
1、fork() //它通过复制调用进程来建立新的进程,它是最基本的进程建立操作。
2.exec //它包括一系列的系统调用,其中每个系统调用都完成相同的功能,即通过用一个新的程序覆盖原内存空间,来实现进程的转变。各种 exec 系统调用之间的区别仅在于它们的参数构造不同。
3.wait()//它提供了初级的进程同步措施,它能使一个进程等待,直到另一个进程结束为止。
4.exit() //这个系统调用常用来终止一个进程的运行
一个进程对应于一个程序的执行,所以绝对不要把进程与程序这两个概念相混淆。进程是动态的概念,而程序为静态的概念。实际上,多个进程可以并发执行同一个程序,对于公用的实用程序就常常是这样。例如,几个用户可以同时运行一个编辑程序,每个用户对此程序的执行均作为一个单独的进程。
二、进程的创建
系统调用fork是建立进程的基本操作,他是把linux变为多任务系统的基础,Linux 系统库 unistd.h 中的函数声明如下
pid_t fork(void);
fork调用成功后,就会使内核建立一个新的进程,所谓的新进程是调用fork的进程的副本。建立的进程被成为子进程(child process),那个调用 fork()建立此新进程的进程被称为父进程(parent process)。以后,父进程与子进程就并发执行,它们都从 fork()调用后的那句语句开始执行。
如图:
![[Pasted image 20250624130244.png]]
它分为 fork()调用前和调用后两部分。调用前的那一部分给出了进程 A 调用fork()的情况。PC(程序计数器)指向当前执行的语句。这时它指向第一个 printf 语句。调用后那一部分给出了调用 fork()以后的情况。这时进程 A 和 B 一起运行,进程 A 是父进程,进程 B 是子进程,它是进程 A 的副本,执行与 A 一样的程序。两个 PC 都指向第二个 printf语句,即 fork()调用之后的语句。也就是说,A 和 B 都从程序的相同点开始执行。
下面讲解系统调用fork情况:
#include <stdio.h>
#include <unistd.h>main()
{pid_t pid;printf(“Now only one process\n”);printf(“Calling fork… \n”);pid=fork();if (!pid)printf(“I’m the child\n”);else if (pid>0)printf(“I’m the parent, child has pid %d\n”,pid);elseprint (“Fork fail!\n”);}
上述代码中,出现了三个分支,当pid为0时,他给出了子进程的工作,大于零时,给出了父进程的工作,小于零时,它给出了 fork 建立子进程失败时所作的工作。当系统那进程总数已达到系统规定的最大数,或者是用户可建立的进程数已达到系统规定的最大数时,这时再调用 fork,则会导致失败,并在 errno 中含有出错代码 EAGAIN。我们还应该注意到。上述两个进程间没有同步措施,所以父进程和子进程的输出内容有可能会叠加在一起。
从上述看出,fork()调用是一个非常有用的系统调用。如果把它隔离起来单独看的话,其似乎是空洞无意义的。但是,当它与其它的 Linux 功能结合起来时,就显现出了它的价值。例如,可以用 Linux 提供的进程间通信机构(如信号和管道等),使父进程与子进程协作完成彼此有关的不同任务。
三、进程的运行
exec系列
Linux 提供了系统调用 exec 系列,它可以用于新程序的运行。exec 系列中的系统调用都完成相同的功能,它们把一个新程序装入调用进程的内存空间,来改变调用进程的执行代码,从而形成新进程。如果 exec 调用成功,调用进程将被覆盖,然后从新程序的入口开始执行。这样就产生了一个新的进程,但是它的进程标识符与调用进程相同。这就是说,exec 没有建立一个与调用进程并发的新进程,而是用新进程取代了原来的进程。所以,对 exec 调用成功后,没有任何数据返回,这与 fork()不同。
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[]);
给出execl调用来运行目录列表程序ls的例子
示例代码:
include <stdio.h>
#include <unistd.h>int main()
{printf(“Executing ls\n”);execl(“/bin/ls”,”ls”,”-l”,NULL);/* 如果 execl返回,说明调用失败 */perror(“execl failed to run ls”);exit(1);
}
用图 3-2 来表示该程序的工作情况。调用前那一部分给出了 execl()即将执行之前时的进程情况,调用后那一部分给出了被改变进程的情况,它现在运行 ls 程序。程序计数器 PC 指向 ls 的第一行,表明 execl()导致从新程序的入口开始执行。
![[Pasted image 20250625101743.png]]
exec 系列的其它系统调用给程序员提供使用 exec 功能的灵活性,它们能适用于多种形式的参数表。execv()只有两个参数:第一个参数指向被执行的程序文件的路径名,第二个参数 argv 是一个字符型指针的数组,
char* argv[]
这个数组中的第一个元素指向被执行程序的文件名(不含路径),剩下的元素指向程序
所用的参数。因为该参数表的长度是不确定的,所以要用 null 指针作结尾。下面给出一个用 execv()运行 ls 命令的例子:
#include <stdio.h>
#include <unistd.h>int main()
{char* av[]={"ls","-l",NULL};execv("/bin/ls",av);perror("execv failed");exit(1);
}
系统调用 execlp()和 execvp()分别类似于系统调用 execl()和 execv(),它们的主要区别是:execlp()和 execvp()的第一个参数指向的是一个简单的文件名,而不是一个路径名。它们通过检索 shell 环境变量 PATH指出的目录,来得到该文件名的路径前缀部分。例如,可以在 shell 中用下述命令序列来设置环境变量 PATH:
PATH=/bin;/usr/bin;/sbin
export PATH
四、进程的控制操作
终止
系统调用 exit()实现进程的终止。exit()在 Linux 系统函数库 stdlib.h 中的函数声明如下
void exit(int status)
exit()除了停止进程的运行外,它还有一些其它作用,其中最重要的是,它将关闭所有
已打开的文件。如果父进程因执行了 wait()调用而处于睡眠状态,那么子进程执行 exit()会
重新启动父进程运行。另外,exit()还将完成一些系统内部的清除工作,例如缓冲区的清除
工作等。
进程的同步
系统调用wait()是实现进程同步的简单手段。声明如下:
pid_t wait(int* status)
当子进程执行时,wait可以暂停父进程的执行,使其等待,一旦子进程执行完,等待的父进程就会重新执行。如果有多个子进程在执行,那么父进程中的wait()在第一个子进程结束时返回,恢复父进程执行。
五、进程属性
每个linux进程都具有一些属性,这些属性可以帮助系统控制和调度进程的运行,以及维持文件系统的安全等。
进程标识符
系统给每个进程定义了一个标识该进程的非负正数,称作进程标识符。当某一进程终
止后,其标识符可以重新用作另一进程的标识符。不过,在任何时刻,一个标识符所代表
的进程是唯一的。系统把标识符 0 和 1 保留给系统的两个重要进程。进程 0 是调度进程,
它按一定的原则把处理机分配给进程使用。进程 1 是初始化进程,它是程序/sbin/init 的执
行。进程 1 是 UNIX 系统那其它进程的祖先,并且是进程结构的最终控制者。
利用系统调用getpid可以得到程序本身的进程标识符,用法如下:
pid =getpid()
利用getppid可以得到父进程的标识符
ppid=getppid()
进程的组表示符
Linux 把进程分属一些组,用进程的组标识符来知识进程所属组。进程最初是通过 fork()
和 exec 调用来继承其进程组标识符。但是,进程可以使用系统调用 setpgrp(),自己形成一
个新的组。setpgrp()在 Linux 系统函数库 unistd.h 中的函数声明如下:
int setpgrp(void)
setpgrp()的返回值 newpg 是新的进程组标识符,它就是调用进程的进程标识符。这时,
调用进程就成为这个新组的进程组首(process group leader)。它所建立的所有进程,将继
承 newpg 中的进程组标识符。
一个进程可以用系统调用 getpgrp()来获得其当前的进程组标识符,getpgrp()在 Linux
系统函数库 unistd.h 中的函数声明如下:
int setpgrp
进程环境
进程的环境是以NULL字符结尾的字符串之集合。在程序中可以用一个以NULL结尾的字符型指针数组来表示它
六、守护进程
概念
守护进程是一种后台运行并且独立于所有终端控制之外的进程。UNIX/Linux 系统通常
有许多的守护进程,它们执行着各种系统服务和管理的任务。
为什么需要有独立于终端之外的进程呢?首先,处于安全性的考虑我们不希望这些进
程在执行中的信息在任何一个终端上显示。其次,我们也不希望这些进程被终端所产生的
中断信号所打断。最后,虽然我们可以通过&将程序转为后台执行,我们有时也会需要程
序能够自动将其转入后台执行。因此,我们需要守护进程。
启动
采取以下几种方式:
1.在系统期间通过系统的初始化脚本启动守护进程。这些脚本通常在目录 etc/rc.d 下,
通过它们所启动的守护进程具有超级用户的权限。系统的一些基本服务程序通常都是通过
这种方式启动的。
2.很多网络服务程序是由 inetd 守护程序启动的。在后面的章节中我们还会讲到它。
它监听各种网络请求,如 telnet、ftp 等,在请求到达时启动相应的服务器程序(telnet server、ftp server 等)。
3.由 cron 定时启动的处理程序。这些程序在运行时实际上也是一个守护进程。
4.由 at 启动的处理程序。
5.守护程序也可以从终端启动,通常这种方式只用于守护进程的测试,或者是重起因
某种原因而停止的进程。
6.在终端上用 nohup 启动的进程。用这种方法可以把所有的程序都变为守护进程,但
在本节中我们不予讨论。
错误输出
守护进程不属于任何的终端,所以当需要某些信息时,他无法像通常程序那样将信息直接输出到标准输出和标准错误输出中。
这就需要某些特殊的机制来处理它的输出。为了解决这个问题,Linux 系统提供了 syslog()系统调用。通过它,守护进程可以向系统的log 文件写入信息。它在 Linux 系统函数库 syslog.h 中的定义如下:
void syslog(int priority,char* format,...);
等级 | 值 | 描述 |
---|---|---|
LOG_EMERG | 0 | 系统崩溃 |
LOG_ALERT | 1 | 必须立即处理的动作 |
LOG_CRIT | 2 | 危急情况 |
LOG_ERR | 3 | 错误 |
LOG_WARNING | 4 | 警告 |
LOG_NOTICE | 5 | 正常但是值得注意的情况(缺省) |
LOG_INFO | 6 | 信息 |
LOG_DEBUG | 7 | 测试信息 |
守护进程的建立
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <syslog.h>
#define MAXFD 64void daemon_init(const char *pname, int facility)
{int i:pid_t pid;/* fork,终止父进程 */if (pid=fork())exit(0);/* 第一子进程 */setsid();signal(SIGHUP,SIG_IGN);/* fork,终止第一子进程 */if (pid=fork())exit(0);/* 第二子进程 */daemon_proc=1;/* 将工作目录设定为"/" */chdir("/");/* 清除文件掩码 */umask(0);/* 关闭所有文件句柄 */for (i=0;i<MAXFD;i++){close(i);}/* 打开 log */openlog(pname,LOG_PID,facility);}
通常进行以下操作
1.fork
首先需要 fork 一个子进程并将父进程关闭。如果进程是作为一个 shell 命令在命令行上
前台启动的,当父进程终止时,shell 就认为该命令已经结束。这样子进程就自动称为了后
台进程。而且,子进程从父进程那里继承了组标识符同时又拥有了自己的进程标识符,这
样保证了子进程不会是一个进程组的首进程。这一点是下一步 setsid 所必须的。
2.setsid
setsid()调用创建了一个新的进程组,调用进程成为了该进程组的首进程。这样,就使
该进程脱离了原来的终端,成为了独立于终端外的进程。
3.忽略 SIGHUP 信号,重新 fork
这样使进程不在是进程组的首进程,可以防止在某些情况下进程意外的打开终端而重
新与终端发生联系。
4.改变工作目录,清除文件掩码
改变工作目录主要是为了切断进程与原有文件系统的联系。并且保证无论从什么地方
启动进程都能正常的工作。清除文件掩码是为了消除进程自身掩码对其创建文件的影响。
5.关闭全部已打开的文件句柄
这是为了防止子进程继承了在父进程中打开的文件而使这些文件始终保持打开从而产
生某些冲突。
6.打开 log 系统
以上就是建立一个守护进程的基本步骤。当然,一个实际的守护进程要比这个例子复
杂许多,但是万变不离其宗,原理都是相同的。通过上面几步,我们可以正确的建立自己
的守护进程