APUE学习:进程控制

进程标识符

进程标识符在系统中是唯一的。Unix采用延迟技术来分配pid。因为,如果一个进程终止了,马上把他的pid分配给新的进程,而且这个新进程与旧进程要做一样的事。那么别人就不知道这是新进程还是旧进程在做了。(类似于tcp4次挥手的最后一步。)
还有一些特殊的进程,例如pid为0的进程,这个可以看成是一个内核的一部分,他主要用来调度。还有pid为1的进程,这个进程就是一个普通的进程了,但是拥有root权限,主要用来当做daemon。1号进程是内核加载完毕之后启动第一个进程,他可以用来设置用户的启动环境,启动内核模块等等。而且他是所有进程的父进程(除了0号进程)

fork函数

fork

fork之后,子进程会复制父进程的data space 与statck,但是现代实现中采用copy-on-write,即子进程在改变的时候才会去拥有自己的进程空间。

注意点:
对于书上Figure 8.1
注意到这两个输出的不同:
1.第一个只是输出了一个 before fork,而第二个输出了两个 before fork。
这是因为在第一个中,我们将标准I/O连接到了终端中,这样标准I/O就是一个行缓冲。所以在调用printf时,会自动刷新缓冲,这样在进程中就没有这个数据了。但是在第二个中,标准I/O被重定向到了文件中,这样标准I/O就是全缓冲了,也就是说除非缓冲区满,或者调用fflush,否则我们是不会刷新缓冲区的。之后我们调用fork创建子进程,子进程会复制父进程的data space ,这样缓冲区中的数据也被复制到子进程中,在子进程退出时因为会自动调用fflush,所以会再次刷新缓冲,造成再次输出before fork
2.注意到write的输出在这两个中都只出现了一次。
这是因为write是不带缓冲的,所以不管写到哪都会只写一次。

思考:1.可以在子进程中调用_exit,这样就不会清空缓冲。
2.printf中不写\n,这样即使是连接到终端也会输出两个before fork

文件共享

在fork之后,子进程与父进程会共享打开文件表。如图:
这里写图片描述

说明:
因为子进程会复制父进程的文件描述符,这样操作不当会造成输出的混乱。所以处理文件描述的方法一般有两个:
1.父进程等待子进程完成。注意子进程会影响到文件偏移。
2.父子进程执行不同的程序段。例如子进程fork之后调用exec。回忆文件描述符的flag中可能会有O_CLOSEXEC,这样子进程exec之后,对应的文件描述符就被关闭了。这样父子进程就会使用不冲突的文件描述符。

使用fork的两种情况

1.父子进程执行不同的代码段
2.子进程要执行不同的程序,可以通过fork之后调用exec来让新程序执行

vfork函数

vfork是为了简化fork之后调用exec的步骤。vfork保证了子进程先进行,直到子进程执行了exit或者exec。vfork实现中,子进程会借用父进程的地址空间,也就是说如果在子进程中改变一些值,会影响到父进程中的值。所以一般vfork是为了spawn用的,即fork-exec。

exit函数

不管如何退出,kernel最终都会执行相同的代码段,即关闭文件描述符,是否内存空间等等。
在一个正常的退出中,是内核,而不是进程来保障终止状态。

wait和waitpid

#include <sys/wait.h>

pid_t wait(int *statloc);

pid_t waitpid(pid_t pid, int *statloc, int options);

//两个函数返回值:若成功则返回进程ID,若出错则返回-1
  • 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。
  • 如果子进程已经终止,并且是一个僵尸进程,则wait立即返回并取得该子进程的状态。
  • waitpid并不等待在其调用之后的第一个终止的子进程。
  • statloc不是NULL,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,可以把stataloc置为NULL。
  • 可以使用宏来知道statloc所表示的意思。
  • wait函数会阻塞,直到子进程结束
  • waitpid会等待指定的进程结束
  • 注意如果子进程已经是僵尸进程,那么 wait会马上返回。

这里写图片描述

  • 对于waitpid中的pid:
    pid == -1,等待任一子进程
    pid > 0, 等待其进程ID与pid相等的子进程
    pid==0, 等待其组ID等于调用进程组ID的任一子进程
    pid < -1 等待其组ID等于pid绝对值的任一子进程

waitpid的option参数

这里写图片描述

waitid

#include <sys/wait.h>

int waitid(idtype_t idtype, id_t id, 
           siginfor_t *infop, int options);

waitid类似于waitpid
idtype:指出要等待的类型,
id:进程id
infop:有更一步的信息
options:

竞争条件

竞争条件:当有多个进程尝试使用共享的数据做些事情,并且最后结果依赖于这些进程的执行顺序。

exec函数族

exec函数族,主要用于在父进程fork之后,调用exec来执行另一个程序,新的程序从它自己的main函数出开始执行。调用exec并不会改变子进程的pID,exec函数仅仅改变了子进程的text,data, heap and stack。
这里写图片描述

  • 最终都会调用execve()函数。

说明:

  • 对于含有filename的exec函数,
    1.如果filename包含一个绝对路径,那么就把文件中的内容当做一个pathname
    2.否则就在PATH环境变量中找可执行文件
  • 如果execlp or execvp函数在PATH环境变量中找到了filename,但是filename不是一个可执行文件,那么就加上他是一个脚本文件,传递给/bin/sh
  • 这七个函数的一个区别在与argument采用list还是vector。注意采用list,那么就要在最有一个argument后面加上(char *) 0表示结束。
  • 如果父进程设置了FD_CLOEXEC,那么在子进程中调用exec就会关闭这个文件。否则还可以使用这个文件。
  • 对于打开的directory streams,在调用exec后就要关闭
  • 在执行exec后,进程的ruid, rgid是不会变的,但是euid与egid根据exec所调用的那个程序是否设置了set-uid与set-gid有关。如果exec的那个程序文件set-uid,那么exec后的进程的euid就是该程序文件的所有者,否则euid还是原来的euid。
    对于set-gid是同理。

改变uid与gid

setuid,setgid

int setuid(uid_t uid);
int setgid(gid_t gid);

说明:

  • setuid与setgid的使用条件:
    1.如果当前进程有root权限,那么就把ruid,euid,suid设置为uid
    2.如果当前进程没有root权限,但是uid等于当前进程的ruid,suid中的一个,那么setuid就将euid设置为uid
    3.否则设置errno为EPERM,并返回-1

  • 1.只有root权限的进程可以改变ruid。通常ruid在登录的时候由登录程序设定好了,并且一般不会改变。
    2.当程序文件的set uid位被设置了,那么在用exec函数之后,euid就会被设置为程序文件的所有者id。如果set uid位没有被设置,那么exec函数就不会去懂euid的值。
    3.对于suid,通常是有exec程序由euid复制得到的。如果程序文件的set uid位被设置了,那么suid就会被复制成euid的值,即程序文件所有者的id。否则就是ruid

  • 改变3个用户id的办法
    这里写图片描述

seteuid, setegid

int seteuid(uid_t uid);
int setegid(gid_t gid);

说明:
对于非root权限进程可以设置他的euid为ruid或者suid。
对于root权限进程只是设置euid为uid

解释器文件

注意使用exec函数一个解释器文件的情况
在exec一个解释器文件的时候,exec的arg[0]会被解释器文件的路径名替代,并且exec的开始的参数会变为解释器文件中的那一行,然后exec中原有的参数会一次右移几位。
解释器文件主要用于脚本中。

system函数

使用system的优势在于他处理了所有的错误,并且所有的信号处理。

说明:
对于Figure 8.25的理解:
注意理解,这里我们的用户shell的ruid与euid都是205,然后运行tsys,这时因为tsys设置了set uid,所以此时我们进程的euid变为了0,然后又去调用了system函数,而system函数又使用了printuids,在printuids我们的权限是ruid = 205, euid = 0。但是我们执行printuids并不用这么高的权限,我们只需要205的euid权限就可以执行了。
所以,在使用system要注意的是,不要去在一个设置了set uid的程序中调用system,这样会造成system调用的那个函数也具有比较高的权限。这种情况要使用fork与exec,即fork之后,使用setXXX等函数降低权限,再去exec。

进程调度

nice函数

int nice(int incr);

说明:
只有root权限的进程可以root来提高自己的优先级,其他权限的用户只能用来降低自己的优先级。

进程时间

这里写图片描述
times函数获得进程的相关时间。注意times返回的就是墙上时钟时间,所以tms结构体中没有墙上时钟时间。

说明:
三种时间:

  • 时钟时间(墙上时钟时间wall clock time):从进程从开始运行到结束,时钟走过的时间,这其中包含了进程在阻塞和等待状态的时间。
  • 用户CPU时间:就是用户的进程获得了CPU资源以后,在用户态执行的时间。
  • 系统CPU时间:用户进程获得了CPU资源以后,在内核态的执行时间。

进程的三种状态为阻塞、就绪、运行。
时钟时间 = 阻塞时间 + 就绪时间 +运行时间
用户CPU时间 = 运行状态下用户空间的时间
系统CPU时间 = 运行状态下系统空间的时间。
用户CPU时间+系统CPU时间=运行时间。

总结

Linux中进程权限这一块的主要思想是:最小权限思想。也就是说我要干一件事,那么要用能做成这件事的最小权限去做。所以有setuid,seteuid函数,就是让我们在fork程序之后,调用这些函数,给子进程适当的权限,然后再去exec。这也是system的一个缺陷,如果在一个设置了setuid的程序中调用system,我们是不可以给system调用的那个函数以适当的权限去做事的。
综上:记住Linux中文件的权限,进程的权限以及最小权限思想。同时记住Linux中一切皆文件的思想。

思考题

1.在一个function中调用vfork,并且返回。
首先在一个function中使用vfork之后,产生一个子进程,这个子进程共享了父进程的stack,包括函数调用栈。由于vfork首先执行子进程,所以会返回子进程的那个值,然后父进程返回时由于子进程已经把调用栈返回了,会出现segment fault错误。
还有一种情况,子进程可以将stack空间都写上自己的值,然后从function中返回,这是main函数栈后面的数据都被改写,也就是说父进程想要在function调用后返回,但是这个时候栈空间被子进程重写了,他就找不到要回到哪里去了,有可能跑到别的地方去了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章