专注于用户空间的系统级编程–即内核之上的所有内容。
程序、进程和线程
程序(program)是指编译过的、可执行的二进制代码,保存在介质上,不运行。
进程(process)是指正在运行的程序。进程包括二进制镜像,加载到内存中,
还涉及到虚拟内存实例、内核资源、安全上下文,以及一个或多个线程。
线程(thread)是进程内的活动单元。每个线程包含自己的虚拟存储器,包括栈、进程状态如寄存器,以及指令指针。
在单线程的进程中,进程即线程。一个进程只有一个虚拟内存实例,一个虚拟处理器。
在多线程的进程中,由于虚拟内存是和进程关联的,所有线程共享相同的内存地址空间。
进程ID
创建新进程的那个进程称为父进程,而新进程称为子进程。
获取进程ID和父进程ID:
#include <sys/types.h>
#inlcude <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
运行新进程
在UNIX中,将程序载入内存并执行程序映像的操作与创建新进程的操作是分离的。
一次系统调用会将二进制程序加载到内存中,替换地址空间原来的内容,
并开始执行。这个过程成为“执行(executing)”一个新的程序,是通过exec系列函数完成。
另一个不同的系统调用用于创建新的进程,相当于复制其父进程。
通常情况下,新的进程会立即执行新的程序。
创建新进程的操作称为派生(fork),是系统调用fork()来完成的。
在新进程中执行一个新的程序需要执行两个步骤:fork()和exec系列函数。
exec系统调用
函数原型如下:
#include <unistd.h>
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[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
exec系列函数只有execve()是真正的系统调用,其他都是基于该系统调用在C库中的封装函数。
这些函数如果调用成功则加载新的程序并跳转到新的程序入口点开始执行,不再返回,如果调用出错则返回-1。
所以,exec函数只有出错的返回值而没有成功的返回值。
函数名和参数解析:
- l和v分别表示参数以列表或数组(向量)方式提供,两种形式的最后一个参数必须为NULL。
由于execl、execlp、execle这三个函数都是可变参数函数,所以NULL应该转换为(char )NULL或(char)0 - p表示会在用户的绝对路径path下查找可执行文件,不需要输入完整路径
- …表示可变长度的参数列表
- e表示会为新进程提供新的环境变量
exec示例代码:
#include <unistd.h>
char *const ps_argv[] = {"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", (char*)0);
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);
execv("/bin/ps", ps_argv);
execvp("ps", ps_argv);
execve("/bin/ps", ps_argv, ps_envp);
函数关系图如下:
fork()系统调用
通过fork()系统调用可以创建一个和当前进程一样的新进程:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
当fork()调用成功时,会创建一个新的进程,几乎与调用fork()的进程完全相同。
这两个进程都会继续运行,调用者进程从fork()返回后,还是照常运行。此处产生分叉点!
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
- 在父进程中,fork返回新创建子进程的进程ID;
- 在子进程中,fork返回0;
- 如果出现错误,fork返回一个负值-1;
可以通过fork返回的值来判断当前进程是子进程还是父进程,fork()系统调用用法:
pid_t pid;
pid = fork();
if(pid > 0)
printf("I am the parent of pid=%d!\n", pid);
else if(0 == pid)
printf("I am the child!\n");
else if(-1 == pid)
printf("fork");
一个进程调用fork()函数后,系统先给新的进程分配资源,
例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,
只有少数值与原来的进程的值不同。相当于克隆了一个自己。
在Linux中,采用写时复制(copy-on-write,COW)的方式,而不是对父进程空间的整体复制。
终止进程
标准函数,用于终止当前进程:
#include <stdlib.h>
void exit(int status);
status值为0表示终止程序成功,用非0值表示异常终止。
标准C语言中数值0和宏EXIT_SCCESS的值表示终止成功,
宏EXIT_FAILURE的值表示终止不成功,其他值的含义由实现定义。
从函数main返回一个整数值相当于用这个值调用exit函数。
在终止进程之前,C库会安顺序执行一下操作步骤:
- 所有通过atexit()或on_exit()函数(废止?)注册的函数按与注册时相反的顺序调用,注册几次就调用几次。
- 刷新打开的是输出流,关闭所有打开的数据流。
- 删除tmpfile()函数生成的临时文件。
- 完成用户空间需要做的所有工作,调用系统调用_exit(),内核可以处理终止进程的剩余工作。
很多时候我们需要在程序退出的时候做一些诸如释放资源的操作,但程序退出的方式有很多种,
比如main()函数运行结束、在程序的某个地方用exit()结束程序、用户通过Ctrl+C或Ctrl+break操作来终止程序等等,
因此需要有一种与程序退出方式无关的方法来进行程序退出时的必要处理。
方法就是用atexit()函数来注册程序正常终止时要被调用的函数。
atexit()函数的参数是一个函数指针,
函数指针指向一个没有参数也没有返回值的函数。atexit()的函数原型是:
int atexit (void (*function)(void));
在一个程序中最多可以用atexit()注册32个处理函数,
这些处理函数的调用顺序与其注册的顺序相反,也即最先注册的最后调用,最后注册的最先调用。
如果进程调用了exec函数,会清空所注册的函数列表。
如果进程是通过信号结束(SIGTERM和SIGKILL),就不会调用这些注册的函数。
等待子进程结束
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
僵尸进程:已经终止,但是它的父进程还没有获取到其状态的进程。
僵尸进程会消耗一些系统资源–仅仅是描述进程之前状态的一些概要信息。
主要是为父进程查询子进程状态时提供相应信息。
一旦父进程得到了想要的信息,内核就清除这些信息,僵尸进程终止。
创建并等待新进程
#include <stdlib.h>
int system(const char *command);
创建新进程并等待它结束,可以看作是同步创建进程。
调用system()会执行参数command所提供的命令,而且还可以为该命令指定参数。
“/bin/sh -c”会作为前缀添加到command参数前面。
通过这种方式,再把整个命令传递给shell。
实际上system()函数执行了三步操作:
- fork一个子进程
- 在子进程中调用exec函数去执行command
- 在父进程中调用wait去等待子进程结束
会话(Session)和进程组
进程组:
- 每个进程都属于某一个进程组
- 每个进程组都有一个进程组号,该号等于该进程组组长的PID号
- 一个进程只能为它自己或子进程设置进程组ID号
会话(session)是一个或多个进程组的集合。
setsid()函数可以建立一个会话:
如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话。
- 此进程变成该对话期的首进程
- 此进程变成一个新进程组的组长进程。
- 此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。
如果该进程是一个进程组的组长,此函数返回错误。 - 使任何进程不成为组长的方式:先调用fork()然后exit()父进程,此时只有子进程在运行
守护进程
Linux Daemon(守护进程)是运行在后台的一种特殊进程。
它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。
Linux系统的大多数服务器就是通过守护进程实现的。
常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。
守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,
所以它是一个由init继承的孤儿进程。
守护进程是非交互式程序,没有控制终端,所以任何输出,
无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
守护进程的名称通常以d结尾,比如sshd、xinetd、crond等
守护进程不想是任何已存在会话的成员,也不想拥有控制终端,创建守护进程所需步骤:
- 调用fork,创建新进程,该进程将会成为守护进程
- 在守护进程的父进程中调用exit。确保父进程的父进程在其子进程结束时会退出,
保证了守护进程的父进程不再继续运行,而且守护进程不是首进程 - 调用setsid函数,使守护进程有一个新的进程组和会话,并作为两者的首进程。
保证了不存在和守护进程相关连的控制终端 - 调用chdir函数,让根目录成为守护进程的工作目录
- 关闭所有文件描述符
- 打开文件描述符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(-1 == pid)
return -1;
else if(0 == pid)
exit(0);
//Create new session and process group
if(setsid() == -1)
return -1;
//Set the working dir to "/"
if(chdir("/") == -1)
return -1;
//Close all open files
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); //stderr
//do its daemon thing ...
}
其实我们完全可以利用daemon()函数创建守护进程,其函数原型:
#include <unistd.h>
int daemon(int nochdir, int noclose);
参数:
- nochdir:=0将当前目录更改至“/”
- noclose:=0将标准输入、标准输出、标准错误重定向至“/dev/null”