这一章主要讲解了进程的相关属性和种类划分,详细介绍了一些进程控制原语
守护进程也叫内核进程,周期性的执行,与普通进程相区别。
内核进程执行的是内核中的函数,而普通进程只有通过系统调用才能执行内核中的函数;
内核进程运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态;
内核进程只能使用大于PAGE_OFFSET(3G)的地址空间,而普通进程不管是用户态还是内核态都可以使用4G的地址空间;
内核进程是由kernel_thread函数在内核态是创建的,普通进程可以由用户自己创建。
内 核提供了很多系统服务(内核进程)供用户程序使用,但这些系统服务不能像库函数(比如printf)那样调用,因为在执行用户程序时CPU处于用户模式, 不能直接调用内核函数,所以需要通过系统调用切换CPU模式,通过异常处理程序进入内核,用户程序只能通过寄存器传几个参数,之后就要按内核设计好的代码 路线走,而不能由用户程序随心所欲,想调哪个内核函数就调哪个内核函数,这样保证了系统服务被安全地调用。在调用结束之后,CPU再切换回用户模式,继续 执行系统调用后面的程序,在用户程序看来就像函数的调用和返回一样。
=========================================================================
介绍一下linux内核的启动过程
传不上去图啊
=========================================================================
进程创建
#include <unistd.h>
pid_t fork(void); 两个返回值,父进程中返回子进程的ID,子进程中返回0
pid_t vfork(void);
COW - copy on write技术
实现过程和区别:转自http://www.diybl.com/course/6_system/linux/Linuxjs/20081013/150563.html
1、 Linux是通过_clone()系统调用来实现fork()的,这一调用通过一系列的参数标志来指明父子进程需要的资源。 fork(),vfork(),clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()函 数,do_fork()函数也就是真正的创建进程的函数。他完成了创建进程的大部分工作。同时他还会调用copy_process()函数。然后让进程开 始运行。
2、vfork()和fork()的功能相同,除了不拷贝父进程的页表项,也就是说不会复制和父进程相关的资源,父子进程将共享 地址空间,子进程对虚拟内存空间的任何实际修改实际上是在修改父进程虚拟内存空间的内容。并且其父进程会被阻塞,直到子进程退出或者执行exec()函数 族.这样由于父子进程共享地址空间,避免了fork在资源复制是的消耗。
如果父子进程共享一块地址空间,那么子进程执行exec时会不会就覆盖掉这块空间了呢?
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int
main(void)
{
pid_t pid;
if((pid = vfork()) < 0)
perror("vfork error:");
else if(pid == 0)
{
execl("/bin/ls","ls",NULL);
_exit(0);
}
printf("sorry,i'm not dead:)/n");//如果被覆盖掉,这句话就不会执行
exit(0);
}
见《Linux内核源代码情景分析》4.4节
sys_vfork()在调用do_fork()时比sys_fork()多了两个标志位,一个是CLONE_VFORK,另一个是CLONE_VM。
vfork():当CLONE_VM标志位为1时,内核并不将父进程的用户空间(数据结构)复制给子进程,而只是将指向mm_struct数据结构的指针复制给子进程,让子进程通过这个指针来共享父进程的用户空间。这样,创建子进程时可以免去复制用户空间的麻烦。而当子进程调用execve()时就可以跳过释放用户空间这一步,直接就为子进程分配新的用户空间。
但是,这样一来省事是省事了,却可能带来新的问题。
fork():在execve()之前,子进程虽然有它自己的一整套代表用户空间的数据结构,但是最终在物理上还是与父进程共用相同的页面。不过,由于子进程有其独立的页面目录与页面表,可以在子进程的页面表里把对所有页面的访问权限都设置成“只读”。这样,当子进程企图改变某个页面的内容时,就会因权限不符而导致页面异常,在页面异常的处理程序中为子进程复制所需的物理页面,这就叫“copy_on_write”。
vfork() 相比之下,如果子进程与父进程共享用户空间,也就是共享包括页面表在内的所有数据结构,那就无法实施“copy_on_write”了。此时子进程所写入 的内容就真正进入了父进程的空间中。我们知道,当一个进程在用户空间运行时,其堆栈也在用户空间。这意味着在这种情况下子进程可以改变父进程的堆栈,反过 来父进程也可以改变子进程的堆栈!因为这个原因,vfork()的使用是很危险的,在子进程尚未放弃对父进程用户空间的共享之前,绝不能让两个进程都进入 系统空间运行。所以,在sys_vfork()调用do_fork()时结合使用了另一个标志位CLONE_VFORK。当这个标志为1时,父进程在创建 了子进程以后就进入睡眠状态,等候子进程通过execve()执行另一个目标程序,或者通过exit()寿终正寝。在这两种情况下子进程都会释放其共享的 用户空间,使父进程可以安全地继续运行。即便如此,也还是有危险,子进程绝对不能从调用 vfork()的那个函数中返回,否则还是可能破坏父进程的返回地址。所以,vfork()实际上是建立在子进程在创建以后立即就会调用execve() 这个前提之上的。
3、写时copy机制:Linux系统采用了“写操作时复制”的方法,其是一种延迟资源复制的方法,子进程在创建的时候并不复制父 进程的相关资源,父子进程通过访问相同的物理内存来伪装已经实现了的对资源的复制。这种共享是制度方式是只读方式,这一点与vfork是不同的,当子进程 对内存数据存在这些的操作时,才会进行资源的复制。正是由于这种机制的出现,vfork()好像已经没有什么作用了。
比较与应用:转自http://blog.chinaunix.net/u2/66039/showart_530124.html
一、fork
1. 调用方法
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
正确返回:在父进程中返回子进程的进程号,在子进程中返回0
错误返回:-1
子进程是父进程的一个拷贝。即,子进程从父进程得到了数据段和堆栈段的拷贝,这些需要分配新的内存;
而对于只读的代码段,通常使用共享内存的方式访问。fork返回后,子进程和父进程都从调用fork函数的下一条语句开始执行。
父进程与子进程的不同之处在于:fork的返回值不同——父进程中的返回值为子进程的进程号,而子进程为0
2. fork函数调用的用途
(1)一个进程希望复制自身,从而父子进程能同时执行不同段的代码。
(2) 进程想执行另外一个程序
二、vfork
1. 调用方法
与fork函数完全相同
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
正确返回:在父进程中返回子进程的进程号,在子进程中返回0
错误返回:-1
2. vfork函数调用的用途
用vfork创建的进程主要目的是用exec函数执行另外的程序,与fork的第二个用途相同
三、fork与vfork的区别
1. fork要拷贝父进程的数据段;而vfork则不需要完全拷贝父进程的数据段,在子进程没有调用exec和exit之前,子进程与父进程共享数据段
2. fork不对父子进程的执行次序进行任何限制;而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制
四、结束子进程
结束子进程不用exit(0),而使用_exit(0)。这是因为_exit(0)在结束进程时,不对标准I/O流进行任何操作。而exit(0)则会关闭进程的所有标准I/O流。
由于父子进程共享文件,故关闭子进程的标准I/O流可能会导致父进程的标准I/O流也同时关闭,会对程序的输入输出产生影响。
===============================================================================
进程终止:exit wait
进程的退出exit函数族
进程有 5 种正常终止的方式和 3 种异常终止的方式。
但是不管进程如何终止都会执行内核中一段相同的代码。这段代码为相应的进程关闭所有打开描述符,释放它所使用的存储器等。
正常1:从main函数返回return。---------正常死亡
正常2:调用exit函数。-----------------自动死亡,留下遗嘱
正常3:调用_exit或_Exit函数。---------自动死亡,没有遗言
正常4:进程的最后一个线程在其启动例程中执行返回语句。但是该线程的返回值不会作为进程的返回值,进程以终止状态0返回;
正常5:进程的最后一个线程调用pthread_exit函数。此时,进程的终止状态总是为0,而与传送给pthread_exit函数的参数没有关系。
异常1:调用abort,产生SIGABRT信号;----------自杀
异常2:进程收到某些信号。----被杀
此信号可以有自身、其他进程或者内核产生;
异常3:最后一个线程对“取消”请求做出响应。
对于正常情况,其返回状态称为退出状态,由进程本身产生;
对于异常情况,其返回状态称为终止状态,由内核产生;
但在最后调用_exit时,内核会将退出状态转换为终止状态。
进程的善后处理wait函数族
当一个进程正常或异常终止时,内核就会向其父进程发送SIGCHLD信号,因为进程终止是个异步事件,所以这种信号也是内核向其父进程发送的异步通知。
转自:http://www.itug.cn/xitong/Unix/20080131/1924.htm
“为什么有了wait函数族还需要使用SIGCHLD信号?”。
一、unix中僵尸进程的含义
关于unix中僵尸进程的含义,APUE2是这样定义:
In UNIX System terminology, a process that has terminated, but whose parent has not yet waited for it, is called a zombie.
也就是说,凡是父进程没有调用wait函数获得子进程终止状态的子进程在终止之后都是僵尸进程,这个概念的关键一点就是父进程是否调用了wait函数。
二、SIGCHLD信号
SIGCHLD信号的含义,APUE2又如是说:
Whenever a process terminates or stops, the SIGCHLD signal is sent to the parent. By default, this signal is ignored, so the parent must catch this signal if it wants to be notified whenever a child's status changes. The normal action in the signal-catching function is to call one of the wait functions to fetch the child's process ID and termination status.
简单的说,子进程退出时父进程会收到一个SIGCHLD信号(内核产生),默认的处理是忽略这个信号,而常规的做法是在这个信号处理函数中调用wait函数获取子进程的退出状态。
三、既然在SIGCHLD信号的处理函数中要调用wait函数族(statloc指针保存子进程终止信号),为什么有了wait函数族还需要使用SIGCHLD信号?
我们知道,unix中信号是采用异步处理某事的机制,好比说你准备去做某事,去之前跟邻居张三说如果李四来找你的话就通知他一声,这让你可以抽身出来去做这件事,而李四真正来访时会有人通知你,这个就是异步信号一个较为形象的比喻。
一般的,父进程在生成子进程之后会有两种情况:一是父进程继续去做别的事情,类似上面举的例子;另一是父进程啥都不做,一直在wait子进程退出。
SIGCHLD信号就是为这第一种情况准备的,它让父进程去做别的事情,而只要父进程注册了处理该信号的函数,在子进程退出时就会调用该函数,在函数中wait子进程得到终止状态之后再继续做父进程的事情。
最后,我们来明确以下二点:
(1)凡父进程不调用wait函数族获得子进程终止状态的子进程在退出时都会变成僵尸进程。
(2)SIGCHLD信号可以异步的通知父进程有子进程退出。
程序清单8-5实现了一种简单实现通过隔代产生进程,干掉第二代,第三代划归init管制的方法来实现后台进程(叫后台进程好像有问题)的方法。
trap -l 来查看信号编号和信号说明性名字的映射关系。
===========================================================================
竞争条件race condition:
方案一:
1,进程等待子进程终止:wait函数
2,子进程等待父进程终止:轮询while(getppid()!=1) sleep(1);
方案二:
信号量,再多个进程之间需要有某种形式的信号发送和接受的方法来协调多个进程之间的竞争关系
初始化信号量,p v 操作(APUE上的tell和wait操作,例子见程序清单8-7)
===========================================================================
进程执行:exec
fork只能让你的进程树分叉,假如你的进程树是苹果树,那么分出来叉还是苹果树的叉,
但通过调用exec函数族,你可以在树干上嫁接各种水果,让你的进程水果树品种齐全(By Professor Hop Lee)。
execlp execl execle
构造argv 构造argv 构造argv
execvp 试每一个path前缀-> execv 使用environ-> execve(系统调用)
上述就是exec函数族的关系,其中只有execve是系统调用,其它的都是对其封装以后的函数。
在函数execl, execlp和execle中, const char *arg 以及省略号代表的参数可被视为 arg0, arg1, ..., argn. 他们合起来描述了指向null结尾的字符串的指针列表, 即执行程序的参数列表. 作为约定, 第一个arg参数应该指向执行程序名自身. 参数列表衉用NULL指针结束!默认第一个参数指向它自身是因为我们在执行main函数时默认的argv[0]也是程序自身的名字,为了保持一致,所以也要求exec函数族也要第一个参数为程序自身的名字。
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
exec 函数族的函数执行成功后不会返回,只留下进程ID 等一些表面上的信息仍保持原样,就好像你那个树干还是有输送水分和营养的能力,但结出来的水果却已经不再是苹果了,可能是桃子,西瓜(我在做梦)。只有调 用失败了,它们才会返回一个 -1,从原程序的调用点接着往下执行。Linux中的进程是大公无私的,当他发现自己结出来的水果卖不出去时,可以嫁接一种更好的水果,来继续保证好的经 营效益。
==========================================================================
进程属性:
用户ID和组ID
进程有时候需要对自己的身份进行修改,通过setid的系列函数来实现进程身份的改变。
详细参见ManPages
信息来源http://bbs.chinaunix.net/viewthread.php?tid=10982
摘要
#include <unistd.h>
int setuid ( uid_t uid );
int seteuid ( uid_t euid );
描述
登录时,RUID、EUID、SUID设置成登录ID。
进程调用exec()执行一个程序文件,考虑两种情况:
a. 程序文件set-user-id,则相应进程EUID、SUID被设置成这个程序文件的属主ID。
b. 程序文件没有set-user-id,则相应进程EUID、SUID不变。
当进程调用seteuid()函数时
a. 如果当前EUID为0,形参euid任意指定。
b. 否则形参euid应该是RUID、EUID、SUID之一。无论如何,最终只影响当前EUID。
进程调用setuid()函数时
a. 如果当前EUID为0,任意调用setuid()同时设置RUID、EUID、SUID。
b. 如果当前EUID不为0,形参uid等于RUID或者SUID,调用setuid()后当前EUID被设置成形参uid,RUID、SUID不受影响。
总结,setuid可以通过以SUID为条件来切换程序文件属主set-user-id的权限和运行程序文件的进程的权限,
a. 调用exec()函数时,赋予权限,见上面exec() RUID=process EUID=owner SUID=owner 调用geteuid保存euid=owner
b. 调用setuid(getuid())收回权限,其中的getuid()得到的是RUID RUID=process EUID=process SUID=owner 其中的参数是通过getuid得到的
c. 调用setuid(euid)(euid是在a步骤通过调用geteuid()得到的) RUID=process EUID=owner SUID=owner 因为SUID这一条件符合euid参数
总结,seteuid和setuid基本上相同,只不过是最终影响的结果只有EUID而已,是对setuid当属主是root是无法正确执行的一种补充。
如上面的abc三个步骤,当文件属主是root时,b步骤中由于a步骤的EUID是root的ID=0,
故调用setuid以后所有的RUID EUID SUID都变成了process,故c步骤就会出错,可以讲b步骤改为seteuid就可以达到目的了。
摘要
#include <unistd.h>
int setreuid ( uid_t ruid, uid_t euid );
描述
setreuid()设置RUID、EUID,最终可能导致SUID改变。
a. 如果形参ruid为-1,RUID不变。如果形参euid为-1,EUID不变。形参ruid、euid可以不同。
b. 如果EUID为0,形参ruid、euid可以是任意合法值。
c. 如果EUID不为0,形参ruid可以等于当前RUID、当前EUID,形参euid可以等于当前RUID、当前EUID、当前SUID。
b c情况下,如果RUID被成功修改(形参ruid不为-1),或者EUID被成功修改(形参euid不为-1)并且不等于最终RUID,则最终SUID被设置成最终EUID。
总结,setreuid可以通过以SUID为临时变量来更加灵活的切换进程的执行权限。
一个set-user-id进程调用setreuid()修改当前EUID成当前RUID后,依然可以调用setreuid()修改当前EUID成当前SUID。
对于一个setuid-to-root的程序,如果需要永久放弃特权,
应该在当前EUID为0的时候调用setuid(not root)放弃特权,或者调用setreuid(not root, not root)(保证EUID不等于RUID,这样SUID会被置位成EUID)放弃特权。
只有这两种正确办法,否则始终有机会重获特权。
如果一个程序setuid-to-<any>应该尽量使用setreuid()永久放弃特权,而不是setuid(),因为这样最通用。
尤其当setuid-to-<not root>;的时候,setuid()根本无法永久放弃特权。
例子,假设当前ruid == 500、euid == 0、suid == 0,
setreuid( -1, 500 ); <-- 临时放弃特权 ruid == 500 euid == 500 suid == 0
setreuid( -1, 0 ); <-- 恢复特权 ruid == 500 euid == 0 suid == 0
setuid( 500 ); <-- 永久放弃特权 ruid == 500 euid == 500 suid == 500
=========================================================================
进程会计
大多数UNIX操作系统都提供了一个选项以进行进程会计处理。启动该选项后每当进程结束时,内核就会写一个会计记录到acct或pacct文件中。
会计记录一般包括命令名,所使用的CPU时间总量,用户ID和组ID。会计文件中记录的顺序对应于进程终止的顺序,而不是启动的顺序。
如果一个进程顺序执行了三个进程,a exec b, b exec c, c exit则只会记录一条,命令名字对应于c,时间是 a b c的总和。
=========================================================================
用户标识
进程时间