linux系统编程读书笔记 第五章

来源:互联网 发布:cad2005软件下载 编辑:程序博客网 时间:2024/06/05 15:46

除非用户显式地指定内核所要运行的程序(通过内核启动的init参数),否则内核就必须寻找一个适合的init程序---这是很少见的内核特定要求中的一个例子。Linux内核会以以下顺序进行尝试:

       1./sbin/initinit最有可能存在的地方。

       2./etc/init:另一个可能存在的地方。

       3./bin/initinit一个可能存在的位置。

       4./bin/shBourne shell的所在的位置,当内核没有找到init时,内核就会尝试运行它。

      在以上候选位置中,第一被发现的就会当作init运行。如果所有的都失败了,内核就会发出panic,挂起系统。

      在内核交出控制后,init会接着完成后续的启动过程。典型情况是init会初始化系统,启动各种服务和启动登陆进程。

 

      缺省情况下,内核将进程ID的最大值限制为32768,系统管理员可以设置/proc/sys/kernel/pid_max的值来突破这个缺省的限制,但会牺牲一些兼容性。

      内核分配进程ID是以严格的线性函数方式进行的。除非pid值达到了最大值,不然内核是不会重用以前已经分配过的值。

      每个进程都被一个用户和组所拥有。每个子进程都继承了父进程的用户和组。

      每个进程都是某个进程组的一部分,子进程通常属于其父进程所在的那个进程组,此外当shell建立了一个管道,所有与管道有关的命令都是同一个进程组的。

      函数getpid()返回调用进程的ID

      函数getppid()返回调用进程的父进程的ID

      函数fork()exec()是和进程创建相关的两个重要函数。具体可以参考apue

      函数execl()成功的调用不仅仅改变了地址空间和进程的映象,还改变了进程的一些属性:

      任何挂起的信号都会丢失;捕捉的任何信号会还原为缺省的处理方式,因为信号处理函数已经不存在于地址空间中了;任何内存的锁定会丢失;多数线程的属性会还原到缺省值;多数关于进程的统计信息会复位;与进程内存相关的任何数据都会丢失,包括映射的文件;包括C语言库的一些特性等独立存在于用户空间的数据都会丢失。

      linux中,exec族的六个函数中只有execve是系统调用,其他的都是在C语言库中封装的函数。因为处理变长参数的系统调用难于实现,并且用户的路径仅存在于用户空间中。

      成功调用时,exec调用不会返回,当失败时,返回-1,并且把errno设置为下列值之一:

      具体看书吧!

      除了以下方面,fork()产生的子进程和父进程非常相近:子进程的id是新分配的;子进程的ppid会设置为父进程的pid;子进程中的资源统计信息会清零;任何挂起的信号会清除,也不会被子进程继承;任何文件锁都不会被子进程所继承。

      调用出错时,不会创建子进程,fork()返回-1。同时会设置相应的errno的值。有两种可能的errno值:

       EAGAIN:内核申请一些资源时失败了,例如新的pid,或者达到了资源限制。

       ENOMEM:没有足够的内核内存来满足所请求的操作。

      写时复制是一个很重要的概念。写时复制在内核中的实现非常简单,与内核页相关的数据结构可以被标记为只读和写时复制。如果有进程试图修改一个页,就会产生一个缺页中断。内核处理缺页中断处理的方式就是对该页进行一次透明复制。这时会清除页面的COW属性,表示着它不再被共享。

      在调用fork时,因为大量的fork之后都会跟着执行exec,那么复制整个父进程地址空间中的内容到子进程的地址空间完全是在浪费时间:如果子进程立刻执行一个新的二进制可执行文件的映像,它先前的地址空间就会被交换出去。写时复制可以对这种情况进行优化。

 

      函数exit()的调用通常会执行一些基本的终止进程的步骤,然后通知内核终止这个进程,这个函数没有办法返回一个错误值。

      终止进程之前,C语言函数执行以下      关闭进程的工作:

       1以在系统中注册的逆序来调用由atexit()on_exit()注册的函数

       2清空所有已经打开的标准IO

       3删除由tmpfile()创建的所有临时文件

      然后就交由_exit()来让内核处理终止进程的剩余工作了

      当进程退出时,内核会清理进程所创建的、不再用到的任何资源。这包括:申请的内存、打开的文件和SystemV的信号量等等。清理完成后,内核摧毁进程,并告知父进程其子进程的终止。

      main函数返回时明确给出一个状态值,或者调用exit(),这是一个良好的编程习惯。

      收到像SIGTERM或者SIGKILL这样的信号也会引起进程的终止,另外一种情况是进程被内核惩罚性的终止。内核会杀死执行非法指令,引起一个段错误,或者内存耗尽的进程。

      函数atexit()用来注册一些在进程结束时要调用的函数。

      atexit()的成功调用会把指定的函数注册到进程正常结束时调用的函数中。如果进程调用了exec,所注册的函数列表会被清除。如果进程是通过信号而结束的,这些注册的函数也不会被调用。要注册的函数必须是无参数的,也没有返回值的。

      函数调用的顺序是和注册的顺序相反的。也就是说这些函数存储在栈中,以后进先出的方式调用。注册的函数不能调用exit(),否则会引起无限的递归调用。

      函数on_exitatexit等价,只是注册原型不一样。

      当一个进程子进程终止时,内核会向其父进程发送SIGCHILD信号。缺省情况下会忽略此信号量,父进程也不会有任何的动作。进程也可通过signal()sigaction()系统调用来有选择的处理这个信号。

      很多的父进程想知道关于子进程终止的更多信息,如子进程的返回值。如果在终止过程中,子进程完全消失了,就没有给父进程留下任何可以来了解子进程的东西。所以如果一个子进程在父进程之前结束,内核应该把子进程设置为一个特殊的状态。处于这种状态的进程叫僵死进程。进程只保留最小的概要信息,一些保存着有用信息的内核数据结构。僵死进程等待着父进程来查询自己的信息。只要父进程获取了子进程的信息,子进程就会消失,否则一直保持僵死状态。

      以前一直搞不懂的wait就是用来实现这个功能的。函数wait返回已终止子进程的pid,或者返回-1表示出错。如果没有子进程终止,调用者会被阻塞,直到一个子进程终止为止。如果子进程终止了,它就会立刻的返回。POSIX标准提供了一些宏来解释wait返回的status的信息,如WIFEXITED

      如果知道需要等待进程的pid,可以使用waitpid()系统调用。函数waitid也是等待子进程结束的函数,其中包括一个指针参数infop,调用成功时,waitid会填充参数infop

      函数wait3wait4用于等待子程序的状态改变,分别有3个参数和4个参数。其与上面的函数最大的不同就是其含有一个rusage参数,rusage参数是一个结构体,其中包括了unshareddata size等数据。这两个函数不是posix定义的,所以最好不要用。

      原来经常使用的system(“pause”)是经典的unix函数!system的调用会使得由参数command指定的程序得到执行,而且程序可以得到相应的参数。

      僵死进程就是一个已经停止了,但是它的父进程还在等待获得其状态的进程。僵死进程还会消耗一些系统资源,用来描述进程曾经的状态。这些资源主要是为了在父进程查询子进程的状态时提供相应的信息。一旦父进程得到了想要的信息,内核就会清除这些信息,僵死的进程就不存在了。

      无论何时,只要有进程结束了,内核就会遍历它的所有子进程,并且把它们的父进程重新设为init进程。这保证了系统中不存在没有父进程的进程。Init进程会周期性的等待所有子进程,确保不会有长时间存在的僵死进程。

      

      与进程相关的用户ID有四个,分别是实际用户ID、有效用户ID、保存设置的用户ID和文件系统用户ID

      实际用户ID是运行这个进程的那个用户的uid。有效用户ID是当前进程所使用的用户ID。通过setuid程序,进程可以改变自己的有效用户ID。准确的说,有效用户ID被设置为拥有此程序文件拥有者的用户ID

      有效用户ID的作用是:它是在检查进程权限过程中使用的用户ID。实际用户ID和保存设置的用户ID像是代理或者一个潜在的用户ID值,它的作用是允许非root进程在这些用户ID之间相互切换。

      函数setuid和函数setgid用来设定当前进程的实际用户ID和实际组ID

      函数seteuid和函数setegid用来设定当前进程的有效用户ID和有效组ID

      每个进程都属于某个进程组。进程组是由一个或多个相互间有关联的进程组成的,它的目的是为了进行作业控制。进程组的主要特征就是信号可以发送给进程组中的所有进程:这个信号可以使同一个进程组中的所有进程终止、停止或者继续运行。

      每个进程组都由进程组ID唯一的标识,并且有一个组长进程。进程组ID就是组长进程的pid。只要在某个进程组中还有一个进程存在,则该进程组就存在。即使组长进程终止了,该进程组依然存在。

      当有新的用户登录计算机,登录进程就会为这个用户创建一个新的会话。这个会话中只有用户的登录SHELL一个进程。登录SHELL作为会话首进程。会话首进程的pid就被作为会话的ID。一个会话就是一个或多个进程组的集合。会话囊括了登录用户的所有活动,并且分配给用户一个控制终端。控制终端是一个用于处理用户IOtty设备。

      会话中的进程组分为一个前台进程组和零个或多个后台进程组。

      在一个指定的系统中,存在着多个会话:每个用户的登录都是一个会话,还有一些与用户登录会话无关的进程也是会话(例如守护进程)。守护进程会创建自己的会话,从而避免与其他存在的会话产生关系。

      函数setsid是一个特殊的系统调用,用来创建一个会话。假如调用进程不是某个进程组组长进程,调用setsid会创建新的会话。调用进程就是这个会话的唯一进程,也是新会话的首进程,但是它没有控制终端。调用也同时在这个会话中创建一个进程组,调用进程成为了组长进程,也是进程组中的唯一进程。新会话ID和进程组ID被设置为调用进程的pid

      与进程组相关的系统调用包括setpgidgetpgid。函数setpgidpid进程的进程组ID设置为pgid

      进程成为守护进程的步骤:

       1调用fork()创建新的进程,它会是将来的守护进程。

       2在守护进程的父进程中调用exit

       3调用setsid,使得守护进程有一个新的进程组和新的会话,两者都把它作为首进程。

       4chdir将当前工作目录改为根目录。

       5关闭所有的文件描述符。

       6打开012号文件描述符(标准输入、标准输出和标准错误),把它们重定向到/dev/null

      以上的步骤很复杂?使用C库函数提供的daemon()就可以了

原创粉丝点击