Linux 系统编程 学习:01-进程的有关概念 与 创建、回收
背景
上一讲介绍了有关系统编程的概念。这一讲,我们针对 进程 开展学习。
概念
进程的身份证(PID)
每一个进程都有一个唯一的身份证号码,称之为进程号PID(Process Identity Number)。
每一个进程都有其双亲进程,称之为父进程(或许称为双亲进程更贴切)。
所有的进程都是祖先进程init的后代,除了init进程,每一个进程都有一个父进程。
通过
pstree
命令,可以清楚地看到系统中各个进程间的内在关系.
程序计数器:程序中即将被执行的下一条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针
进程优先级
优先级:相对于其他进程的优先级。
并行和并发
- 并行:指两个或者多个事件在同一时刻发生;
- 并发:指两个或多个事件在同一时间间隔发生
程序和进程
程序:二进制文件,占用磁盘空间
进程:运行着的程序,数据在内存中,占用系统资源、CPU、物理内存
进程的 有关流程
每个进程都有自己的生命周期,包括创建、执行、终止和删除。操作系统的运行过程中不断的重复这些过程,因此从操作系统性能的角度来看,进程的生命周期非常重要。
父进程可以通过系统调用fork()
创建子进程。
fork()
调用后将创建子进程的描述符和进程ID,在子进程中复制父进程的进程描述符,同时并不复制父进程的地址空间,而是在父进程的地址空间中运行。
exec(
系统调用将在子进程的地址空间中复制新的程序数据。
由于共享相同的地址空间,写入新的程序数据将会导致内存的页错误,因此,在这种情况下,Linux内核将分配新的物理页给子进程。
一般情况下,子进程执行自己的程序,避免了复制完整地址空间的低效率操作。
当程序执行完成时,子进程通过系统调用exit()
终止进程。
系统调用exit()
释放子进程相应的资源,并发送信号给父进程,通知子进程的终止。在这个时刻,子进程被称为僵尸进程。
父进程通过系统调用wait()接收子进程的终止信号。当父进程接收到该信号,删除子进程所有的数据结构,并释放子进程的进程描述符,这个时候子进程才被完全删除。
进程的创建
在一个进程中创建另外一个进程,新创建的进程就为子进程。
在Linux中主要提供了fork、vfork、clone三个进程创建方法。
fork:
#include <unistd.h>
pid_t fork(void); // pit_t -> unsigned long int
RETURN VALUE
On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1
is returned in the parent, no child process is created, and errno is set appropriately.
/*
成功时,返回两个值:
对于父进程,fork函数返回子程序的进程号,
而对于子程序,fork函数则返回零。
失败: 返回-1
*/
使用fork创建一个新进程后,由于其基于copy-on-write机制(也就是子进程与父进程初始时只有页表和task structure不同),不会立即将父进程的进程分布复制一份给子进程。而对于父进程在fork前所使用的资源,子进程继承了大部分,如父进程打开的文件描述符,还有部分没有继承。(例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。)
复制出来的子进程有自己的task_struct结构和pid,这一步所做的是复制。这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如: pipe,共享内存等机制, 另外通过fork创建子进程,需要将上面描述的每种资源都复制一个副本。这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的。
子进程从fork后开始执行。
fork之后,子进程继承父进程的信号处理方式。(当一个进程调用fork时,因为子进程在开始时复制父进程的存储映像,信号捕捉函数的地址在子进程中是有意义的,所以子进程继承父进程的信号处理方式。)
关于 子进程 fork 时的 资源继承
父进程创建子进程是为了其能够协助父进程完成某些操作。因此,父进程必须将其自己的一些资源分享给子进程,以便父子进程共同完成任务。
子进程继承了父进程的几乎所有的属性:
- 实际UID、GID和有效UID,GID、附加GID
- 调用exec()时的关闭标志
- UID设置模式比特位、GID设置模式比特位
- 进程组号、会话ID、控制终端、环境变量
- 当前工作目录、根目录
- 文件创建掩码UMASK、文件长度限制ULIMIT
- 预定值, 如优先级和任何其他的进程预定参数, 根据种类不同决定是否可以继承,一些其它属性
但子进程也有与父进程不同的属性:
- 进程号,子进程号与任何一个活动的进程组号不同
- 父进程号
- 子进程继承父进程的文件描述符或流时, 具有自己的一个拷贝;并且与父进程和其它子进程共享该资源
- 子进程的用户时间和系统时间被初始化为0
- 子进程的超时时钟设置为0
- 子进程不继承父进程的记录锁
- pending signals 也不会被继承
vfork:
不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。
vfork创建子进程后,父进程会被阻塞,直到子进程调用exit或exec函数族(exec,将可执行文件载入到地址空间并执行。)
注意: 用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
一般而言,如果想调用一个外部程序,则是以vfork来创建一个子进程,通过子进程来调用exec函数组来实现的。
因为exec的系列函数有一个特点:
如果使用了exec函数,那么系统会将被调用的命令装载入当前进程中,当前进程的内容将全部消失取而代之的是调用的命令(相当于进程重生,exec系列函数有关介绍 )
特点:
1)共享父进程的所有资源
2)父进程在创建完子进程后,等到子进程调用exec函数或者结束之后再运行
3)调用vfork时,其中的代码要求尽可能少(因为容易不稳定)
4)vfork的函数必须以exec或exit结束
clone
系统调用fork()和vfork()是无参数的,而clone()则带有参数。
fork()是全部复制,vfork()是共享内存,而clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags来决定。另外,clone()返回的是子进程的pid。
实际上, Linux
clone
系统调用是fork
和pthread_create
的通用形式,它允许调用者指定在调用进程和新创建的进程之间共享哪些资源。clone()的主要用途是实现线程:在共享内存空间中并发运行的程序中的多个控制线程。
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
/* Prototype for the raw system call */
long clone(unsigned long flags, void *child_stack,
void *ptid, void *ctid,
struct pt_regs *regs);
等待回收子进程的函数
等待回收子进程的函数有2个,wait
与waitpid
,它们和exit配套使用。wait或waitpid调用只能清理一个子进程,清理多个子进程需要用到循环
wait
等待任意一个子进程,将子进程的返回值填入到wait里面的status中。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
当子进程调用exit(x),(x会被&0377之后才传给wait)
status:接受子进程的退出状态标志。(如果为空,则表示父进程不关心子进程的终止状态)
如果想获取到正确的exit的返回值,你可以调用WEXITSTATUS(status)来获取,(子进程退出时的返回值被系统处理了,若想得到正常的返回值,那就得调用WEXITSTATUS宏来操作,因为其他位数做为标志位使用了。)
waitpid:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
**pid: **
>0: 等待id值为pid的子进程
0: 等待所在进程组当中的任一子进程
-1: 等待任一子进程,相当于 wait
<-1: 等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
**status: 接受子进程的退出状态标志: **(同 上文)
**options: **
- WNOHANG:若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0
- WUNTRACED:返回终止子进程信息和因信号停止的子进程信息
- WCONTINUED:返回收到SIGCONT信号而恢复执行的已停止子进程状态信息
成功返回清理掉的子进程PID;如果非阻塞返回0代表没有子进程退出;如果返回-1则是失败
wait(),waitpid()区别:
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞;
- waitpid()可以控制它所等待的进程;
例程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid = -1;
int ret;
int status;
pid = fork();
if(pid == 0) // 子进程入口
{
sleep(2);
printf("Sun p\n");
exit(123);
}else if(pid > 0) // 父进程入口
{
printf("Father p\n");
ret = wait(&status);
printf("%d\n", status);
printf("%d\n", WEXITSTATUS(status));
}
return 0;
}
守护进程
Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。
Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd 、 web服务器httpd 、 邮件服务器sendmail 和 数据库服务器mysqld等。
守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
守护进程的名称通常以d结尾,比如sshd、xinetd、crond等
进程组 :
- 每个进程也属于一个进程组
- 每个进程主都有一个进程组号,该号等于该进程组组长的PID号 .
- 一个进程只能为它自己或子进程设置进程组ID号
会话期: - 会话期(session)是一个或多个进程组的集合。
编写守护进程的一般步骤步骤:
1)在父进程中执行fork并exit退出;
2)在子进程中调用setsid函数创建新的会话;
3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
4)在子进程中调用umask函数,设置进程的umask为0;
5)在子进程中关闭任何不需要的文件描述符
注册退出函数
有三个函数可以正常退出程序:_exit
,_Exit
和exit
。
内核执行程序的唯一方式是exec函数被调用。程序自动终止的唯一方式是_exit或者_Exit被调用,或者程序被动的被信号终止。
#include<stdlib.h>
void _exit(int status);
void _Exit(int status); // The function _Exit() is equivalent to _exit().
#include <unistd.h>
void exit(int status);
exit:退出进程之前先去执行atexit或者是on_exit注册的函数,清理了IO缓冲,关闭该进程使用的所有fd之后才退出进程
在main里面return的时候默认会调用exit这个函数(即:main函数return(0)和exit(0)是一样的。)
_exit:直接退出进程,不清空IO缓冲区
我们重点来看 atexit
与 on_exit
atexit
atexit:注册一个退出函数 (在结束时函数被注册多少次就会被调用多少次,后入先出。)
在一个程序中最多可以用atexit()
注册 ATEXIT_MAX (32)个处理函数, 后注册的函数先执行。
通过
fork
创建子进程时,它将继承其父进程的注册副本。成功调用exec
函数之一后,将删除所有注册。
#include <stdlib.h>
int atexit(void (*function)(void));
参数解析:
- function:exit后执行的处理函数名,必须是
void function(void);
的格式
返回值:成功返回0;失败返回非零。
例程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void bye(void)
{
printf("That was all, folks\n");
}
int main(void)
{
long a;
int i;
a = sysconf(_SC_ATEXIT_MAX);
printf("ATEXIT_MAX = %ld\n", a);
i = atexit(bye);
if (i != 0) {
fprintf(stderr, "cannot set exit function\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
on_exit
on_exit:复杂的退出处理函数,用来设置程序正常结束(调用exit() 或从main 中返回)前调用的函数。
函数的作用是:通过exit或从程序的main返回,注册在正常进程终止时要调用的给定函数。函数被传递给上一次调用exit的status参数和on_exit的arg参数。
可以注册多次:每次注册调用一次。后注册的函数先执行。
通过
fork
创建子进程时,它将继承其父进程的注册副本。成功调用exec
函数之一后,将删除所有注册。
#include <stdlib.h>
int on_exit(void (*function)(int , void *), void *arg);
参数解析:
function:exit后执行的处理函数名字
- 其中里面的int代表 退出的返回值
- void * 注册时传递的arg,这份数据不会被拷贝,依赖于调用时的情况。
arg:参数arg指针会传给参数function函数
#include <stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h>
void my_exit(int status, void *myarg)
{
printf("my_exit before exit()!\n");
printf("exit (%d)\n", status);
printf("arg = %s\n", (char*)myarg);
}
void my_exit2(int status, void *myarg)
{
printf("my_exit2 before exit()!\n");
printf("exit (%d)\n", status);
printf("arg = %s\n", (char*)myarg);
}
int main(int argc, char *argv[])
{
char str[10]="test";
printf("Father's PID is %d\n", getpid());
on_exit(my_exit, (void *)str);
on_exit(my_exit, (void *)str);
if (fork() == 0)
{
// son
printf("Son's PID is %d\n", getpid());
str[0] = 'b';
on_exit(my_exit, (void *)str);
on_exit(my_exit2, (void *)str);
exit(getpid());
}
exit(getpid());
}