Programs, Progresses, and Threads
一个二进制文件是位于一个诸如磁盘的存储介质上被编译的,可执行代码。通俗来说,我们可以用术语program,大型重要的二进制文件我们也可以称为应用。
一个progress是一个正在运行的program。
一个progress包括加载到内存中的二进制image,但也包含更多:虚拟化内存的实例、内核资源(如打开的文件)、安全上下文(如关联用户)、 一个或多个thread。
一个thread是进程内部活动的单位。每个线程都有自己的虚拟处理器,其中包括堆栈、处理器状态(如寄存器)和指令指针。
在单个线程的进程中,进程就是线程。有一个虚拟化内存实例和一个虚拟化处理器。在多线程进程中,有多个线程。 虚拟内存与进程相关联,线程都共享相同的内存地址空间。
Process ID 进程ID
每个进程由一个唯一的标识符表示,即进程ID(通常缩短为pid)。
The Progress Hierarchy 进程的层级
产生新进程的进程称为父进程;新进程称为子进程。
每个进程都是从另一个进程派生出来的(当然,init进程除外)。因此,每个子进程都有父进程。此关系记录在每个进程的父进程ID(ppid)中, 它是子进程的父进程的pid。
每个子进程继承其父进程的用户和组所有权。
pid_t
以编程方式上来讲,进程ID由类型表示,该类型在头文件<sys/ypes.h>中定义。在Linux上,pid_t通常是int类型的。
获取进程ID和父进程ID
getpid()系统调用返回调用进程的进程ID:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
getppid()系统调用返回调用进程的父进程的进程ID:
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
两个调用都不会返回错误。
//usage
printf("My pid=%jd\n", (intmax_t)getpid());
printf("Parent's pid=%jd\n", (intmax_t)getppid());
运行一个新的进程
一个系统调用将二进制程序加载到内存中,替换以前地址空间的内容,并开始执行新程序。这被称为执行一个新的程序,功能上是由exec系统调用家族提供的。
使用不同的系统调用来创建一个新进程。通常,新进程会立即执行新程序。
创建一个新过程的行为叫做forking,这个功能由系统调用fork()提供。
首先,要在新进程中执行一个新程序,就需要fork来创建一个新进程,然后exec将一个新二进制文件加载到该进程中。
exec函数系列
没有单一的exec函数;相反,有一个基于单个系统调用的exec函数系列。
让我们首先看看其中最简单的调用execl():
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int ret;
ret = execl("bin/vi", "vi", NULL);
if(ret == -1)
perror("execl");
int ret;
ret = execl("/bin/vi", "vi", "/home/kidd/hooks.txt", NULL);
if(ret == -1)
perror("execl")
The rest of the family
#include <unistd.h>
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 *filename,
char *const argv[],
char *const envp[]);
l和v描述参数是通过列表·还是通过数组(vector)提供的。
p代表系统的PATH被用来搜索要执行的文件。
e代表一个新的环境也被提供给新的进程。
//execvp() sample
int ret;
ret = execvp("vi", "vi", "/home/kidd/hooks.txt", NULL);
if(ret == -1)
perror("execvp");
const char *args[] = { "vi", "/home/kidd/hooks.txt", NULL };
int ret;
ret = execv ("/bin/vi", args);
if (ret == −1)
perror ("execvp");
在Linux中,只有EXEC家族的一个成员是一个系统调用。其余的是围绕系统调用的C库中的封装。因为千变万化的系统调用很难实现,而且 用户路径的概念只存在于用户空间中,惟一的系统调用选项是execve()。系统调用原型与用户调用相同。也就是说其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。
fork()系统调用
运行与当前image相同的新进程可以通过fork()系统调用创建
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void)
在子节点中,成功调用fork()将返回0。在父类中,fork()返回子节点的pid。
//Usage Sample
pid_t pid;
pid = fork();
if(pid>0)
printf("I am the parent of pid = %d!\n", pid);
else if(!pid)
printf("I am the child!\n");
else if(pid == -1)
perror("fork");
下面的示例生成一个运行二进制/bin/winlass的新进程
pid_t pid;
pid = fork ();
if (pid == −1)
perror ("fork");
/* the child ... */
if (!pid) {
const char *args[] = { "windlass", NULL };
int ret;
ret = execv ("/bin/windlass", args);
if (ret == −1) {
perror ("execv");
exit (EXIT_FAILURE);
}
}
Copy-on-write
Copy-on-write 是一种懒散的优化策略,旨在减少复制资源的开销。
前提很简单:如果多个使用者请求对自己的资源副本进行读取访问,则不需要复制资源副本。
如果使用者确实试图修改其资源的副本,此时该资源被透明地复制,并且该副本被给予修改的使用者。消费者然后可以修改其资源的副本,而其他使用者则继续共享原始的、未更改的版本。
这就是名字的由来:复制仅在写入时发生。
Terminating a Process 结束一个进程
POSIX和C89都定义了终止当前进程的标准函数:
#include <stdlib.h>
void exit(int status);
具体来说,Status&0377返回给父程序。
EXIT_SUCCESS和EXIT_FAILURE被定义为表示成功和失败的可移植方式。在linux上,0通常表示成功;一个非零的值,如1或−1, 表示失败。
//一个成功的exit
exit(EXIT_SUCCESS);
在终止进程之前,C库执行以下关闭步骤:
- 以注册的相反顺序调用使用atexit()或on_exit()注册的任何函数。
- 刷新所有打开的标准I/O stream。
- 删除使用tmpfile()函数创建的所有临时文件。
当进程退出时,内核将清理它代表进程创建的不再使用的所有资源。这包括但不限于已分配的内存、打开的文件、 和SystemV信号量。清理后,内核将销毁进程并通知父级其子进程的终止。
其他关闭进程的方法
结束程序的经典方法不是通过显式的系统调用,而是简单地“falling off the end” of the program。
C/C++中就是指从main函数返回。
显式返回退出状态是一个很好的编码实践,可以通过exit()或者从main()return一个值。
注意,成功的返回是EXIT(0),或者是main()的return 0。
如果进程被发送信号,其默认操作是终止该进程,则该进程也可以终止。这些信号包括SIGTERM和SIGKILL。
结束程序执行的最后一种方法是引起内核的愤怒。内核可以终止用于执行非法指令的进程,比如说导致段错误,消耗完内存,消耗更多允许的资源,等等。
atexit()
atexit()库调用,用于注册在进程终止时调用的函数。
#include <stdlib.h>
int atexit(void(*function)(void));
如果进程通过信号终止,则不调用已注册的函数。
当进程通过exit()或从main()返回终止时已注册的函数运行。
POSIX标准要求atexit()至少支持ATEXIT_MAX个·注册函数,并且该值至少为32。通过sysconf()和_SC_ATEXIT_MAX可以得到精确的最大值。
long atexit_max;
atexit_max = sysconf(_SC_ATEXIT_MAX);
printf("atexit_max=%ld\n", atexit_max);
On success, atexit() returns 0. On error, it returns −1.
//Sample
void out(void)
{
long atexit_max;
atexit_max = sysconf(_SC_ATEXIT_MAX);
printf("atexit() succeeded! atexit_max=%ld\n",
atexit_max);
}
int atexitSample() {
if(atexit(out)){
fprintf(stderr, "atexit() failed!\n");
}
return 0;
}
int main(int argc, char*argv[])
{
atexitSample();
printf("Alex is cool!\n");
return 0;
}
atexit()测试结果
on_exit()
SunOS 4定义了自己的与atexport()等价的内容,Linux的glibc支持它:
#include <stdlib.h>
int on_exit (void (*function)(int, void *), void *arg);
这个函数的工作原理与atexit()相同,但是注册函数的原型是不同的:
void my_function (int status, void *arg);
建议应该使用符合标准的atexit()。
SIGCHLD
当进程终止时,内核将信号SIGCHLD发送给父进程。默认情况下,此信号将被忽略,父级不采取任何操作。进程可以选择处理此信号,可以通过使用signal()或Sigaction()系统调用函数。
Waiting for Terminated Child Processes 等待子进程结束
Unix的原始设计者决定,当子进程在其父进程之前死亡时,内核应该将子进程置于一个特殊的进程状态。处于这种状态的进程称为僵尸。只有一些包含潜在有用数据的基本内核数据结构的基本框架被保留下来。处于此状态的进程等待其父进程查询其状态。 (一种称为等待僵尸进程的过程)。只有在父进程获得有关已终止子进程的保留信息之后,进程才会正式退出。
Linux内核提供了几个接口,用于获取关于被结束子类的信息。POSIX定义的最简单的此类接口是wait():
#include <sys/types.h>
#include <sys.wait.h>
pid_t wait(int *status);
对wauit()的调用返回终止的子进程的PID或在错误时返回-1。
如果没有子项终止,则调用将阻塞,直到子项终止。
所以早收到SIGCHILD时再调用wait()将不会阻塞。
status指针包含有关子进程的信息。
由于POSIX允许实现根据它们认为合适的情况定义状态中的位,因此标准提供了一系列宏来解释参数:
#include <sys/wait.h>
int WIFEXITED (status);
int WIFSIGNALED (status);
int WIFSTOPPED (status);
int WIFCONTINUED (status);
int WEXITSTATUS (status);
int WTERMSIG (status);
int WSTOPSIG (status);
int WCOREDUMP (status);
- 如果进程正常终止,return或者exit, 则WIFEXITED返回true。在本例中,宏WEXITSTATUS提供了传递给_EXIT()的低阶八位。
- 如果一个信号导致进程终止,WIFSIGNALED返回true。
在这种情况下,WTERMSIG返回导致终止的信号数目,如果进程响应于接收到信号而产生了core dump,则WCOREDUMP返回true。
WIFSTOPPED和WIFCONTINUED返回true(如果进程已停止或继续),当前正在通过ptrace()系统调用进行跟踪。只有在实现调试器时才会应用。
如果WIFSTOPPED为真,则WSTOPSIG提供停止进程的信号。
int main(int argc, char*argv[])
{
int status;
pid_t pid;
if(!fork()){
return 1;
}
pid = wait(&status);
if(pid == -1){
perror("wait");
}
printf("pid=%d\n", pid);
if(WIFEXITED(status)){
printf("Normal termination with exit status=%d\n",
WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){
printf("Killed by signal=%d%s\n",
WTERMSIG(status),
WCOREDUMP(status)?"(dumped core)": "");
}
if(WIFSTOPPED(status)){
printf("Stopped by signal = %d\n",
WSTOPSIG(status));
}
if(WIFCONTINUED(status)){
printf("Continued\n");
}
return 0;
}
fork之后return 1
int main(int argc, char*argv[])
{
int status;
pid_t pid;
if(!fork()){
abort();
}
pid = wait(&status);
if(pid == -1){
perror("wait");
}
printf("pid=%d\n", pid);
if(WIFEXITED(status)){
printf("Normal termination with exit status=%d\n",
WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){
printf("Killed by signal=%d%s\n",
WTERMSIG(status),
WCOREDUMP(status)?"(dumped core)": "");
}
if(WIFSTOPPED(status)){
printf("Stopped by signal = %d\n",
WSTOPSIG(status));
}
if(WIFCONTINUED(status)){
printf("Continued\n");
}
return 0;
}
fork之后调用·abort()
Waiting for Specific Process 等待指定的进程
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int &status, int options);
pid参数指定要等待的进程。它的值可以分为4个组:
- < -1
等待进程组ID等于该值绝对值的任何子进程。例如,传递−500等待进程组500中的任何进程。 - -1
等待任何子进程。这是与wait()相同的行为。 - 0
等待与调用进程属于同一进程组的任何子进程。 - > 0
等待任何子进程,其pid恰好是所提供的值。
status参数和wait()是一样的。
options参数是以下选项中的零或多个的二进制OR
- WNOHANG
不阻塞,如果没有匹配的子进程已经终止(或停止或继续),则立即返回。 - WUNTRACED
如果设置了WUNTRACED,则会设置返回状态参数中的WIFSTOPPED位,即使调用进程没有跟踪子进程。此标志允许实现更一般的作业控制,比如说在shell中。 - WCONTINUED
如果设置,即使调用进程没有跟踪子进程,也会设置返回状态参数中的WIFCONTINUED位。与WUNTRACED一样,此标志对于实现shell非常有用。
成功后,waitpid()返回状态已更改状态的进程的pid。如果指定了WNOHANG,并且指定的子进程·或多个子进程尚未更改状态,那么返回0。如果·发生错误,返回−1。
int status;
pid_t pid;
pid = waitpid (1742, &status, WNOHANG);
if (pid == −1)
perror ("waitpid");
else {
printf ("pid=%d\n", pid);
if (WIFEXITED (status))
printf ("Normal termination with exit status=%d\n",
WEXITSTATUS (status));
if (WIFSIGNALED (status))
printf ("Killed by signal=%d%s\n",
WTERMSIG (status),
WCOREDUMP (status) ? " (dumped core)" : "");
}
wait(&status) == waitpid(-1 &status, 0)
Even more Waiting Versatility
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id,
siginfo_t *infop, int options);
waitid()用于等待和获取有关子进程的状态更改(终止、停止、继续)的信息
idtype和id参数指定要等待的子对象。
idtype可以由如下的值:
- P_PID
等待一个pid与参数id匹配的子进程。 - P_GIP
等待一个进程组ID与参数id匹配的子进程。
-P_ALL
等待任意的子进程,id值忽略。
参数options可以值下列值的二进制OR形式组成:
- WEXITED
调用将等待已终止的子进程(由id和idtype确定)。 - WSTOPPED
该调用将等待响应接收signal而停止执行的子进程。 - WCONTINUED
调用将等待那些在接收到信号后继续执行的子进程。 - WNOHANG
调用永远不会阻塞,但是如果没有匹配的子进程(已经终止,或停止,或继续),则会立即返回 。 - WNOWAIT
调用不会从僵尸状态中删除匹配进程。这一过程今后可能会等待。
在成功地等待一个子进程时,wawtid()将填写infop参数,该参数必须指向有效的siginfo_t类型。这其中如下的参数会被填写: - si_pid 子进程的pid
- si_uid 子进程的uid
- si_code
设置为CLD_EXECED、CLD_KELD、CLD_STEST或CLD_CONJ续,分别响应子进程终止、通过信号死亡、通过信号停止或继续通过信号。 - si_signo
设置为SIGCHLD - si_status
如果si_code是CLD_EXTEXT,则该字段是子进程的退出代码。否则,此字段是发送给导致状态更改的子进程的信号的代码。
成功后,watid()返回0。如果出现错误,witid()将返回−1
Launching ans Waiting for a New Process
如果一个进程生成一个子进程只是为了立即等待它的终止,那么使用这个接口是有意义的:
#define _XOPEN_SOURDE
#include <stdlib.h>
int system(const char *command);
使用system()运行简单的实用程序或shell脚本是很常见的。
通常,目标是简单地获得它的返回值。
command参数是参数/bin/sh -c的后缀。
在成功的情况下,返回值是wait()提供的命令的返回状态, 通过WEXITSTATUS状态获取已执行命令的退出代码。
如果调用/bin/sh本身失败,则WEXITSTATUS给出的值与Exit(127)返回的值相同。
如果出现错误,则调用返回−1。
如果命令为NULL,如果shell/bin/sh可用,system()返回一个非零值,否则返回0。
do{
int ret;
ret = system ("pidof rudderd");
if (WIFSIGNALED (ret) &&
(WTERMSIG (ret) == SIGINT ||
WTERMSIG (ret) == SIGQUIT))
break; /* or otherwise handle */
}while(1);
Zombies
Users and Groups
软件开发中的最佳实践鼓励最低特权原则,这意味着流程应该以尽可能最低的权限执行。
Real, Effective, and Saved User and Group IDs
事实上,与进程关联的user ID不是一个,而是四个:
- real
- effective
- saved
- filesystem
real user ID是最初运行进程的用户的uid。也就是父进程的uid。
effective user ID 是进程当前使用的uid。
saved used ID是进程的远离啊的effective user ID。
Changing the Effective User or Group ID
Linux提供了两个POSIX授权的函数,用于设置当前正在执行的进程的effective 用户ID和组ID:
#include <sys/types.h>
#include <unistd.h>
int seteuid (uid_t euid);
int setegid (gid_t egid);
seteuid() returns 0. On failure, it returns −1。
Support for Saved User IDs
Obtaining the User and Group IDs
这两个系统调用返回real 的用户和组ID,
#include <unistd.h>
#include <sys/types.h>
uid_t getuid (void);
gid_t getgid (void);
这两个系统调用分别返回effective用户ID和组ID。
#include <unistd.h>
#include <sys/types.h>
uid_t geteuid (void);
gid_t getegid (void);
Sessions and Process Group
每个进程是一个进程组的成员,
进程组的主要属性是可以向组中的所有进程发送信号:单个操作可以终止、停止或继续同一进程组中的所有进程。
每个进程组由进程组ID(pgid)标识,并具有一个process group leader。
进程组ID等于进程leader的pid。
sessions, process groups, processes和控制终端之间的关系
Session System Calls
shell在登录时创建新会话。他们通过一个特殊的系统调用来做到这一点,这使得创建一个新会话变得很容易:
#include <unistd.h>
pid_t setsid (void);
换句话说,setsid()在新session中创建一个新的progress group,并使调用的过程成为两者的领导者。
确保任何给定进程不是进程组领导的最简单方法是分叉,让父进程终止,并让子进程执行setsid()。例如:
pid_t pid;
pid = fork ();
if (pid == −1) {
perror ("fork");
return −1;
} else if (pid != 0)
exit (EXIT_SUCCESS);
if (setsid () == −1) {
perror ("setsid");
return −1;
}
获取当前session ID(虽然不太有用)也是可以的:
#define _XOPEN_SOURCE 500
#include <unistd.h>
pid_t getsid (pid_t pid);
如果PID参数为0,则调用返回调用进程的会话ID。
getsid()的使用不常见,主要用于诊断目的:
pid_t sid;
sid = getsid (0);
if (sid == −1)
perror ("getsid"); /* should not be possible */
else
printf ("My session id=%d\n", sid);
Process Group System Calls
调用setpgid()将进程pid的进程组ID设置为pgid
#define _XOPEN_SOURCE 500
#include <unistd.h>
int setpgid (pid_t pid, pid_t pgid);
如果pid参数为0,则使用当前进程。如果pgid为0,则使用pid标识的进程ID作为进程组ID。
与会话一样,获取进程的进程组ID也是可能的,尽管用处不大:
#define _XOPEN_SOURCE 500
#include <unistd.h>
pid_t getpgid (pid_t pid);
如果PID为0时,则使用当前进程的进程组ID。
与getsid()一样,使用主要用于诊断目的:
pid_t pgid;
pgid = getpgid (0);
if (pgid == −1)
perror ("getpgid"); /* should not be possible */
else
printf ("My process group id=%d\n", pgid);
Daemons 守护进程
守护进程是在后台运行的进程,而不是连接到任何控制终端。
守护进程有两个一般要求:它必须作为init的子程序运行,并且不能连接到终端。
通常,程序执行以下步骤成为守护进程:
- 调用fork()。这将创建一个新进程,它将成为守护进程。
2.在父进程中,调用exit()。这确保了父进程的父父进程(守护进程的祖父母)确信它的子进程终止,守护进程的父进程不再运行,并且守护进程不是进程组的领导。
3.调用setsid(),为守护进程提供一个新的进程组和会话,这两个进程组和会话都将其作为领导者。这也确保了进程没有相关的控制终端(因为进程刚刚创建了一个新会话,并且不会分配一个会话)。
4.通过chdir()将工作目录更改为根目录。这是因为继承的工作目录可以在文件系统上的任何位置。守护进程倾向于在系统正常运行期间运行,您不想让某个随机目录处于打开状态,从而防止广告 管理员从卸载包含该目录的文件系统。
5.关闭所有文件描述符。您不希望继承打开的文件描述符,并且在不知情的情况下将其保持为打开状态。
6.打开文件描述符 0、1和2(标准输入、标准输出和标准错误)并将它们重定向到/dev/null。
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/fs.h>
int main (void) {
pid_t pid;
int i;
/* create new process */
pid = fork ();
if (pid == −1)
return −1;
else if (pid != 0)
exit (EXIT_SUCCESS);
/* create new session and process group */
if (setsid () == −1)
return −1;
/* set the working directory to the root directory */
if (chdir ("/") == −1)
return −1;
/* close all open files--NR_OPEN is overkill, but works */
for (i = 0; i < NR_OPEN; i++)
close (i);
/* redirect fd's 0,1,2 to /dev/null */
open ("/dev/null", O_RDWR); /* stdin */
dup (0); /* stdout */
dup (0); /* stderror */
/* do its daemon thing... */
return 0;
}
大多数Unix系统在其C库中提供了一个守护进程()函数,使这些步骤自动化,将繁琐的操作变成简单的操作:
#include <unistd.h>
int daemon (int nochdir, int noclose);
如果nochdir为非零,守护进程将不会将其工作目录更改为根目录。如果noclose为非零,守护进程将不会关闭所有打开的文件描述符。如果下列情况下,这些选项是有用的 父进程已经设置了。然而,通常情况下,这两个参数的值都是0。
成功后,调用返回0。如果失败,调用将返回−1。
网友评论