简单介绍一下 Linux 中的常见的一些与进程相关的操作,主要是执行命令、守护进程等。
Linux 上将进程创建和新进程加载分开,分别通过 fork()
和 execNN()
执行,其中后者包含了一组可用的函数,包括了 execl
、execlp
、execle
、execv
、execvp
。
创建了一个进程后,再将子进程替换成新的进程,其声明如下:
#include <unistd.h>
extern char **environ;
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[]);
其中,path
表示要启动程序的名称包括路径名;arg
表示启动程序所带的参数,一般第一个参数为要执行命令名,不是带路径且 arg 必须以 NULL 结束。
注意,上述 exec 系列函数底层都是通过 execve()
系统调用实现。
#include <unistd.h>
int execve(const char *filename, char *const argv[],char *const envp[]);
其中各个函数的区别如下。
包括了 execl、execlp、execle,表示后边的参数以可变参数的形式给出,且都以一个空指针结束。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("entering main process---\n");
execl("/bin/ls","ls","-l",NULL);
printf("exiting main process ----\n");
return 0;
}
利用 execl 将当前进程 main 替换掉,所有最后那条打印语句不会输出。
也就是 execlp、execvp,表示第一个参数 path 不用输入完整路径,只有给出命令名即可,它会在环境变量 PATH 当中查找命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("entering main process---\n");
execlp("ls","ls","-l",NULL);
printf("exiting main process ----\n");
return 0;
}
包括了 execv、execvp 表示命令所需的参数以 char *arg[]
形式给出,且 arg 最后一个元素必须是 NULL 。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("entering main process---\n");
char *argv[] = {"ls","-l",NULL};
execvp("ls", argv);
printf("exiting main process ----\n");
return 0;
}
包括了 execle ,可以将环境变量传递给需要替换的进程,再上述的声明中,有一个指针数组的变量 extern char **environ;
,每个指针指向的字符串为 KV 结构。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
char *const envp[] = {"AA=11", "BB=22", NULL};
printf("entering main process---\n");
execle("/bin/bash", "bash", "-c", "echo $AA", NULL, envp);
printf("exiting main process ----\n");
return 0;
}
没有终端限制,让某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。
守护进程的编程本身并不复杂,复杂的是各种版本的 Unix 的实现机制不尽相同,造成不同 Unix 环境下守护进程的编程规则并不一致。
守护进程的特性包括了:
/etc/rc.d
中启动,crond
启动,还可以由用户终端执行。编程步骤包括了。
创建子进程,父进程退出,在进程中调用 fork()
,然后使父进程终止,让 Daemon 在子进程中后台执行,此时在形式上脱离了控制终端。
进程属于一个进程组,进程组号 (GID) 就是进程组长的进程号;会话可以包含多个进程组,这些进程组共享一个控制终端;这个控制终端通常是创建进程的登录终端。
控制终端、登录会话和进程组通常是从父进程继承下来的,我们的目的就是要摆脱它们,使之不受它们的影响。在此通过调用 setsid()
创建新的会话+进程组,并使当前进程成为会话组长。
注意,当进程是会话组长时 setsid()
调用失败,但第一步已保证该进程不是会话组长。调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端,可以通过使进程不再成为会话组长来禁止进程重新打开控制终端。
一般来说,也就是再次 fork()
一次,不过这个是可选的。
进程从创建它的父进程那里继承了打开的文件描述符,如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。
一般需要关闭 0~2 标准输出。
进程活动时,其工作目录所在的文件系统不能卸下,一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 chdir("/tmp")
。
进程从创建它的父进程那里继承了文件权限掩模,它可能修改守护进程所创建的文件的存取位,为防止这一点,将文件创建掩模清除 umask(0)
。
注意,设置掩码时,使用的是八进制,例如 umask(022)
。
处理 SIGCHLD
信号并不是必须的,但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程 (zombie) 从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。
在 Linux 下可以简单地将 SIGCHLD
信号的操作设为 SIG_IGN,signal(SIGCHLD, SIG_IGN)
,这样,内核在子进程结束时不会产生僵尸进程,这一点与 BSD4 不同,BSD4 下必须显式等待子进程结束才能释放僵尸进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
int main(void)
{
FILE *fp;
time_t t;
int pid;
int i;
printf("Before fork , pgid=%d, ppid=%d, pid=%d, sid=%d\n",
getpgid(getpid()), getppid(), getpid(), getsid(getpid()));
pid = fork(); // STEP 1
if (pid < -1) { // error
perror("fork()");
exit(EXIT_FAILURE);
} else if (pid != 0) { // parent
fprintf(stdout, "Parent PID(%d) running\n", pid);
exit(EXIT_SUCCESS);
}
printf("Forked before sid, pgid=%d, ppid=%d, pid=%d, sid=%d\n",
getpgid(getpid()), getppid(), getpid(), getsid(getpid()));
/* child */
setsid(); // STEP 2, Detach from session.
printf("Forked after sid , pgid=%d, ppid=%d, pid=%d, sid=%d\n",
getpgid(getpid()), getppid(), getpid(), getsid(getpid()));
pid = fork(); // STEP 3, fork again to prevent recreate a console.
if (pid < -1) { // error
perror("fork()");
exit(EXIT_FAILURE);
} else if (pid != 0) { // parent
fprintf(stdout, "Parent PID(%d) running\n", pid);
exit(EXIT_SUCCESS);
}
printf("After second fork, pgid=%d, ppid=%d, pid=%d, sid=%d\n",
getpgid(getpid()), getppid(), getpid(), getsid(getpid()));
sleep(1);
printf("Sleep for a while, pgid=%d, ppid=%d, pid=%d, sid=%d\n",
getpgid(getpid()), getppid(), getpid(), getsid(getpid()));
for(i = 0; i < getdtablesize(); i++) // STEP 4, close all opend file discript.
close(i);
chdir("/tmp"); // STEP 5
umask(022); // STEP 6
while (1) {
fp = fopen("test.log", "a");
if(fp != NULL ) {
t = time(0);
fprintf(fp, "I'm here at %s\n", asctime(localtime(&t)) );
fclose(fp);
}
sleep(60);
}
}
可以通过如下命令查看当前进程的状态。
$ ps -axo pid,ppid,pgid,sid,state,comm | grep test
实际上,在上述的第二步中,可以再执行一次 fork()
操作。
第一次 fork()
后子进程继承了父进程的进程组 ID,但有一个新进程 ID,这就保证了子进程不是一个进程组的首进程。
然后 setsid()
是为了跟主进程的 SID PGID 脱离设置成子进程的 SID PGID,此时其父进程是 1 ,SID PGID 均等于 PID 。
虽然此时子进程已经被 init 接管了,但是只有 setsid()
之后才算是跟那个主进程完全脱离,不受他的影响 (原进程组、会话组被 kill 后该进程不会退出)。
第二次 fork()
不是必须的,主要目的是为了防止进程再次打开一个控制终端(暂时不知道如何打开)。
因为打开一个控制终端的前提条件是该进程必须是会话组长,那么再 fork()
一次后,子进程 ID 不再等于 sid (sid 是进程父进程的 sid),所以也无法打开新的控制终端。
此时这个子进程是首进程了,然后此时为了避免他是首进程,所以又 fork()
了一次,此时其父进程是上次 fork()
的进程,当父进程退出后其父进程变为 1 。
及时已经关闭所有的文件描述符,那么打印消息时仍然会打印到原终端。
另外一种说法是为了防止出现僵尸进程。
每次调用父进程必须要保证可以快速退出,否则会导致子进程的父进程 ID 仍然为原进程,如果此时子进程先退出就会成为僵尸进程,即使已经调用了 setsid()
。
无论如何都必须要保证父进程的快速推出,否则不管是 fork 了几次,仍然会出现僵尸进程。
如果喜欢这里的文章,而且又不差钱的话,欢迎打赏个早餐 ^_^
This Site was built by Jin Yang, generated with Jekyll, and hosted on GitHub Pages
©2013-2018 – Jin Yang