一、进程与程序
注册进程终止处理函数 atexit()
#include <stdlib.h>
int atexit(void (*function)(void));
使用该函数需要包含头文件<stdlib.h>。
函数参数和返回值含义如下:
function:函数指针,指向注册的函数,此函数无需传入参数、无返回值。
返回值:成功返回 0;失败返回非 0。
测试
#include <stdio.h>
#include <stdlib.h>
static void bye(void)
{
puts("Goodbye!");
}
int main(int argc, char *argv[])
{
if (atexit(bye))
{
fprintf(stderr, "cannot set exit function\n");
exit(-1);
}
exit(0);
}
1、何为进程?
进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。
2、进程号
Linux 系统下的每一个进程都有一个进程号(processID,简称 PID),进程号是一个正数,用于唯一标识系统中的某一个进程。在 Ubuntu 系统下执行 ps 命令可以查到系统中进程相关的一些信息,包括每个进程的进程号
在应用程序中,可通过系统调用 getpid()来获取本进程的进程号,其函数原型如下所示:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid = getpid();
printf("本进程的 PID 为: %d\n", pid);
exit(0);
}
获取父进程
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
使用示例
用法如上。
二、进程的环境变量
在 shell 终端下可以使用 env 命令查看到 shell 进程的所有环境变量
export LINUX_APP=123456 # 添加 LINUX_APP 环境变量
export -n LINUX_APP # 删除 LINUX_APP 环境变量
1、应用程序中获取环境变量
在我们的应用程序中只需申明它即可使用,如下所示:
extern char **environ; // 申明外部全局变量 environ
测试
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[])
{
int i;
/* 打印进程的环境变量 */
for (i = 0; NULL != environ[i]; i++)
puts(environ[i]);
exit(0);
}
获取指定环境变量 getenv()
#include <stdlib.h>
char *getenv(const char *name);
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
const char *str_val = NULL;
if (2 > argc)
{
fprintf(stderr, "Error: 请传入环境变量名称\n");
exit(-1);
}
/* 获取环境变量 */
str_val = getenv(argv[1]);
if (NULL == str_val)
{
fprintf(stderr, "Error: 不存在[%s]环境变量\n", argv[1]);
exit(-1);
}
/* 打印环境变量的值 */
printf("环境变量的值: %s\n", str_val);
exit(0);
}
2、添加/删除/修改环境变量
putenv()函数
#include <stdlib.h>
int putenv(char *string);
putenv()函数将设定 environ 变量(字符串数组)中的某个元素(字符串指针)指向该 string 字符串,而不是指向它的复制副本,因此,不能随意修改参数 string 所指向的内容,这将影响进程的环境变量,出于这种原因,参数 string 不应为自动变量(即在栈中分配的字符数组)。
测试
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (2 > argc)
{
fprintf(stderr, "Error: 传入 name=value\n");
exit(-1);
}
/* 添加/修改环境变量 */
if (putenv(argv[1]))
{
perror("putenv error");
exit(-1);
}
exit(0);
}
setenv()函数
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (3 > argc)
{
fprintf(stderr, "Error: 传入 name value\n");
exit(-1);
}
/* 添加环境变量 */
if (setenv(argv[1], argv[2], 0))
{
perror("setenv error");
exit(-1);
}
exit(0);
}
除了上面给大家介绍的函数之外,我们还可以通过一种更简单地方式向进程环境变量表中添加环境变量,用法如下:
NAME=value ./app
在执行程序的时候,在其路径前面添加环境变量,以 name=value 的形式添加,如果是多个环境变量,则在./app 前面放置多对 name=value 即可,以空格分隔。
unsetenv()函数
#include <stdlib.h>
int unsetenv(const char *name);
3、清空环境变量
通过将全局变量 environ 赋值为 NULL来清空所有变量。
environ = NULL;
也可通过 clearenv()函数来操作,函数原型如下所示:
#include <stdlib.h>
int clearenv(void);
clearenv()函数内部的做法其实就是将environ赋值为NULL。在某些情况下,使用setenv()函数和clearenv()函数可能会导致程序内存泄漏,前面提到过,setenv()函数会为环境变量分配一块内存缓冲区,随之称为进程的一部分;而调用 clearenv()函数时没有释放该缓冲区(clearenv()调用并不知晓该缓冲区的存在,故而也无法将其释放),反复调用者两个函数的程序,会不断产生内存泄漏。
4、环境变量的作用
环境变量常见的用途之一是在 shell 中,每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表示用户的家目录,USER 环境变量表示当前用户名,SHELL 环境变量表示 shell 解析器名称,PWD 环境变量表示当前所在目录等,在我们自己的应用程序当中,也可以使用进程的环境变量。
三、进程的内存布局
C 语言程序一直都是由以下几部分组成的:
1、正文段:这是 CPU 执行的机器语言指令部分,文本段具有只读属性。
2、初始化数据段:包含了显式初始化的全局变量和静态变量。
3、未初始化数据段:包含了未进行显式初始化的全局变量和静态变量。
4、栈:函数内的局部变量以及每次函数调用时所需保存的信息都放在此段中,函数传递的实参以及函数返回值等也都存放在栈中。
5、堆:可在运行时动态进行内存分配的一块区域,譬如使用 malloc()分配的内存空间。
四、进程的虚拟地址空间
在 Linux 系统中,每一个进程都在自己独立的地址空间中运行,在 32 位系统中,每个进程的逻辑地址空间均为 4GB,这 4GB 的内存空间按照 3:1 的比例进行分配,其中用户进程享有 3G 的空间,而内核独自享有剩下的 1G 空间。
所有应用程序运行在自己的虚拟地址空间中,使得进程的虚拟地址空间和物理地址空间隔离开来,这样做带来了很多的优点:
⚫ 进程与进程、进程与内核相互隔离。一个进程不能读取或修改另一个进程或内核的内存数据,这是因为每一个进程的虚拟地址空间映射到了不同的物理地址空间。提高了系统的安全性与稳定性。
⚫ 在某些应用场合下,两个或者更多进程能够共享内存。因为每个进程都有自己的映射表,可以让不同进程的虚拟地址空间映射到相同的物理地址空间中。通常,共享内存可用于实现进程间通信。
⚫ 便于实现内存保护机制。譬如在多个进程共享内存时,允许每个进程对内存采取不同的保护措施,例如,一个进程可能以只读方式访问内存,而另一进程则能够以可读可写的方式访问。
⚫ 编译应用程序时,无需关心链接地址。前面提到了,当程序运行时,要求链接地址与运行地址一致,在引入了虚拟地址机制后,便无需关心这个问题。
五、fork()创建子进程
使用 fork()创建子进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid = fork();
switch (pid)
{
case -1:
perror("fork error");
exit(-1);
case 0:
printf("这是子进程打印信息<pid: %d, 父进程 pid: %d>\n",getpid(), getppid());
_exit(0); //子进程使用_exit()退出
default:
printf("这是父进程打印信息<pid: %d, 子进程 pid: %d>\n",getpid(), pid);
exit(0);
}
}
使用示例 2
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid = fork();
switch (pid)
{
case -1:
perror("fork error");
exit(-1);
case 0:
printf("这是子进程打印信息\n");
printf("%d\n", pid);
_exit(0);
default:
printf("这是父进程打印信息\n");
printf("%d\n", pid);
exit(0);
}
}
在 exit()函数之前添加了打印信息,而从上图中可以知道,打印的 pid 值并不相同,0 表示子进程打印出来的,46953 表示的是父进程打印出来的,所以从这里可以证实,fork()函数调用完成之后,父进程、子进程会各自继续执行 fork()之后的指令,它们共享代码段,但并不共享数据段、堆、栈等,而是子进程拥有父进程数据段、堆、栈等副本,所以对于同一个局部变量,它们打印出来的值是不相同的,因为 fork()调用返回值不同,在父、子进程中赋予了 pid 不同的值。
六、父、子进程间的文件共享
调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于 dup(),这也意味着父、子进程对应的文件描述符均指向相同的文件表,如下图所示:
由此可知,子进程拷贝了父进程的文件描述符表,使得父、子进程中对应的文件描述符指向了相同的文件表,也意味着父、子进程中对应的文件描述符指向了磁盘中相同的文件,因而这些文件在父、子进程间实现了共享,譬如,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置偏移量。
父、子进程同时对文件进行写入操作,测试代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
pid_t pid;
int fd;
int i;
fd = open("./test.txt", O_RDWR | O_TRUNC);
if (0 > fd)
{
perror("open error");
exit(-1);
}
pid = fork();
switch (pid)
{
case -1:
perror("fork error");
close(fd);
exit(-1);
case 0:
/* 子进程 */
for (i = 0; i < 4; i++) //循环写入 4 次
write(fd, "1122", 4);
close(fd);
_exit(0);
default:
/* 父进程 */
for (i = 0; i < 4; i++) //循环写入 4 次
write(fd, "AABB", 4);
close(fd);
exit(0);
}
}
再来测试另外一种情况,父进程在调用 fork()之后,此时父进程和子进程都去打开同一个文件,然后再对文件进行写入操作,测试代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
pid_t pid;
int fd;
int i;
pid = fork();
switch (pid)
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
fd = open("./test.txt", O_WRONLY);
if (0 > fd)
{
perror("open error");
_exit(-1);
}
for (i = 0; i < 4; i++) //循环写入 4 次
write(fd, "1122", 4);
close(fd);
_exit(0);
default:
/* 父进程 */
fd = open("./test.txt", O_WRONLY);
if (0 > fd)
{
perror("open error");
exit(-1);
}
for (i = 0; i < 4; i++) //循环写入 4 次
write(fd, "AABB", 4);
close(fd);
exit(0);
}
}
这种文件共享方式实现的是一种两个进程分别各自对文件进行写入操作,因为父、子进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况。
fork()函数使用场景
fork()函数有以下两种用法:
⚫ 父进程希望子进程复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的,父进程等待客户端的服务请求,当接收到客户端发送的请求事件后,调用 fork()创建一个子进程,使子进程去处理此请求、而父进程可以继续等待下一个服务请求。
⚫ 一个进程要执行不同的程序。譬如在程序 app1 中调用 fork()函数创建了子进程,此时子进程是要去执行另一个程序 app2,也就是子进程需要执行的代码是 app2 程序对应的代码,子进程将从 app2程序的 main 函数开始运行。这种情况,通常在子进程从 fork()函数返回之后立即调用 exec 族函数来实现,关于 exec 函数将在后面内容向大家介绍。
系统调用 vfork()
一般不用
七、fork()之后的竞争条件
调用 fork 之后,无法确定父、子两个进程谁将率先访问 CPU,也就是说无法确认谁先被系统调用运行(在多核处理器中,它们可能会同时各自访问一个 CPU),这将导致谁先运行、谁后运行这个顺序是不确定的。
这个时候可以通过采用采用某种同步技术来实现,譬如前面给大家介绍的信号,如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它,示例代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
static void sig_handler(int sig)
{
printf("接收到信号\n");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t wait_mask;
/* 初始化信号集 */
sigemptyset(&wait_mask);
/* 设置信号处理方式 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGUSR1, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
switch (fork())
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程开始执行\n");
printf("子进程打印信息\n");
printf("~~~~~~~~~~~~~~~\n");
sleep(2);
kill(getppid(), SIGUSR1); //发送信号给父进程、唤醒它
_exit(0);
default:
/* 父进程 */
if (-1 != sigsuspend(&wait_mask))//挂起、阻塞,当执行完信号处理函数后或者接收到wait_mask以外的信号后解挂,将掩码设成调用前的值
exit(-1);
printf("父进程开始执行\n");
printf("父进程打印信息\n");
exit(0);
}
}
八、监视子进程
1、wait()函数
系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息,其函数原型如下所示:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
函数参数和返回值含义如下:
status:参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。
返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1。
系统调用 wait()将执行如下动作:
⚫ 调用 wait()函数,如果其所有子进程都还在运行,则 wait()会一直阻塞等待,直到某一个子进程终止;
⚫ 如果进程调用 wait(),但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那么 wait()将返回错误,也就是返回-1、并且会将 errno 设置为 ECHILD。
⚫ 如果进程调用 wait()之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用 wait()也不会阻塞。wait()函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子进程的一些资源,俗称为子进程“收尸”。
参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,可以通过以下宏来检查 status 参数:
⚫ WIFEXITED(status):如果子进程正常终止,则返回 true;
⚫ WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit()时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过WEXITSTATUS 宏转换;
⚫ WIFSIGNALED(status):如果子进程被信号终止,则返回 true;
⚫ WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
⚫ WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main(void)
{
int status;
int ret;
int i;
/* 循环创建 3 个子进程 */
for (i = 1; i <= 3; i++)
{
switch (fork())
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(i);
_exit(i);
default:
/* 父进程 */
break;
}
}
sleep(1);
printf("~~~~~~~~~~~~~~\n");
for (i = 1; i <= 3; i++)
{
ret = wait(&status);
if (-1 == ret)
{
if (ECHILD == errno)
{
printf("没有需要等待回收的子进程\n");
exit(0);
}
else
{
perror("wait error");
exit(-1);
}
}
printf("回收子进程<%d>, 终止状态<%d>\n", ret,
WEXITSTATUS(status));
}
exit(0);
}
2、waitpid()函数
使用 wait()系统调用存在着一些限制,这些限制包括如下:
⚫ 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
⚫ 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
⚫ 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
函数参数和返回值含义如下:
pid:参数 pid 用于表示需要等待的某个具体子进程,关于参数 pid 的取值范围如下:
⚫ 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
⚫ 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
⚫ 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;
⚫ 如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。
status:与 wait()函数的 status 参数意义相同。
options:稍后介绍。
返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0,稍后介绍。
参数 options 是一个位掩码,可以包括 0 个或多个如下标志:
⚫ WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
⚫ WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
⚫ WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main(void)
{
int status;
int ret;
int i;
/* 循环创建 3 个子进程 */
for (i = 1; i <= 3; i++)
{
switch (fork())
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(i);
_exit(i);
default:
/* 父进程 */
break;
}
}
sleep(1);
printf("~~~~~~~~~~~~~~\n");
while(1)
{
ret = waitpid(-1, &status, WNOHANG); //轮询
if (0 > ret)
{
if (ECHILD == errno)
exit(0);
else
{
perror("wait error");
exit(-1);
}
}
else if (0 == ret)
continue;
else
printf("回收子进程<%d>, 终止状态<%d>\n", ret,WEXITSTATUS(status));
}
exit(0);
}
3、僵尸进程与孤儿进程
父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程。
如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。
4、SIGCHLD 信号
当发生以下两种情况时,父进程会收到该信号:
⚫ 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号;
⚫ 当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。
通过 SIGCHLD 信号实现异步方式监视子进程。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
static void wait_child(int sig)
{
/* 替子进程收尸 */
printf("父进程回收子进程\n");
while(waitpid(-1, NULL, WNOHANG) > 0)// waitpid()返回 0,表明再无僵尸进程存在;或者返回-1,表明有错误发生
continue;
}
int main(void)
{
struct sigaction sig = {0};
/* 为 SIGCHLD 信号绑定处理函数 */
sigemptyset(sig.sa_mask);
sig.sa_handler = wait_child;
sig.sa_flags = 0;
if (-1 == sigaction(SIGCHLD, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
/* 创建子进程 */
switch (fork())
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(1);
printf("子进程结束\n");
_exit(0);
default:
/* 父进程 */
break;
}
sleep(3);
exit(0);
}
九、执行新程序
1、execve()函数
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
函数参数和返回值含义如下:
filename:参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
argv:参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main(int argc, char *argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。argv[0]对应的便是新程序自身路径名。
envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value。
返回值:execve 调用成功将不会返回;失败将返回-1,并设置 errno。
使用示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char *arg_arr[5];
char *env_arr[5] = {"NAME=app", "AGE=25","SEX=man", NULL};
if (2 > argc)
exit(-1);
arg_arr[0] = argv[1];
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execve(argv[1], arg_arr, env_arr);
perror("execve error");
exit(-1);
}
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[])
{
char **ep = NULL;
int j;
for (j = 0; j < argc; j++)
printf("argv[%d]: %s\n", j, argv[j]);
puts("env:");
for (ep = environ; *ep != NULL; ep++)
printf(" %s\n", *ep);
exit(0);
}
代码中中 execve()函数的使用并不是它真正的应用场景,通常由 fork()生成的子进程对 execve()的调用最为频繁,也就是子进程执行 exec 操作。
说到这里,我们来分析一个问题,为什么需要在子进程中执行新程序?其实这个问题非常简单,虽然可以直接在子进程分支编写子进程需要运行的代码,但是不够灵活,扩展性不够好,直接将子进程需要运行的代码单独放在一个可执行文件中不是更好吗,所以就出现了 exec 操作。
父进程绑定的信号处理函数对子进程的影响
fork后子进程会继承父进程绑定的信号处理函数,若调用exec加载新程序后,就不会继承这个信号处理函数了。
父进程的信号掩码对子进程的影响
fork后子进程会继承父进程的信号掩码,执行exec后仍然会继承这个信号掩码。
2、system()函数
使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令
#include <stdlib.h>
int system(const char *command);
例子
system("ls -la")
system("echo HelloWorld")
使用示例
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int ret;
if (2 > argc)
exit(-1);
ret = system(argv[1]);
if (-1 == ret)
fputs("system error.\n", stderr);
else
{
if (WIFEXITED(ret) && (127 == WEXITSTATUS(ret)))
fputs("could not invoke shell.\n", stderr);
}
exit(0);
}
3、进程状态与进程关系
3.1、进程状态
Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
3.2、进程关系
主要包括:无关系(相互独立)、父子进程关系、进程组以及会话。
十、守护进程
1、何为守护进程
是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:
⚫ 长期运行。
⚫ 与控制终端脱离。
守护进程是一种很有用的进程。Linux 中大多数服务器就是用守护进程实现的,譬如,Internet 服务器inetd、Web 服务器 httpd 等。同时,守护进程完成许多系统任务,譬如作业规划进程 crond 等。
2、编写守护进程程序
编写守护进程一般包含如下几个步骤:
-
创建子进程、终止父进程
-
子进程调用 setsid 创建会话
setsid 函数能够使子进程完全独立出来,从而脱离所有其他进程的控制。 -
将工作目录更改为根目录
-
重设文件权限掩码 umask
设置文件权限掩码的函数是 umask,通常的使用方法为 umask(0)。 -
关闭不再需要的文件描述符
-
将文件描述符号为 0、1、2 定位到/dev/null
-
其它:忽略 SIGCHLD 信号
接下来,我们根据上面的介绍的步骤,来编写一个守护进程程序,示例代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int main(void)
{
pid_t pid;
int i;
/* 创建子进程 */
pid = fork();
if (0 > pid)
{
perror("fork error");
exit(-1);
}
else if (0 < pid)//父进程
exit(0); //直接退出
/*
*子进程
*/
/* 1.创建新的会话、脱离控制终端 */
if (0 > setsid())
{
perror("setsid error");
exit(-1);
}
/* 2.设置当前工作目录为根目录 */
if (0 > chdir("/"))
{
perror("chdir error");
exit(-1);
}
/* 3.重设文件权限掩码 umask */
umask(0);
/* 4.关闭所有文件描述符 */
for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
close(i);
/* 5.将文件描述符号为 0、1、2 定位到/dev/null */
open("/dev/null", O_RDWR);
dup(0);
dup(0);
/* 6.忽略 SIGCHLD 信号 */
signal(SIGCHLD, SIG_IGN);
/* 正式进入到守护进程 */
for ( ; ; )
{
sleep(1);
puts("守护进程运行中......");
}
exit(0);
}