进程标识符在系统中是唯一的。Unix采用延迟技术来分配pid。因为,如果一个进程终止了,马上把他的pid分配给新的进程,而且这个新进程与旧进程要做一样的事。那么别人就不知道这是新进程还是旧进程在做了。(类似于tcp4次挥手的最后一步。)
还有一些特殊的进程,例如pid为0的进程,这个可以看成是一个内核的一部分,他主要用来调度。还有pid为1的进程,这个进程就是一个普通的进程了,但是拥有root权限,主要用来当做daemon。1号进程是内核加载完毕之后启动第一个进程,他可以用来设置用户的启动环境,启动内核模块等等。而且他是所有进程的父进程(除了0号进程)
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
说明:
因为子进程会复制父进程的文件描述符,这样操作不当会造成输出的混乱。所以处理文件描述的方法一般有两个:
1.父进程等待子进程完成。注意子进程会影响到文件偏移。
2.父子进程执行不同的程序段。例如子进程fork之后调用exec。回忆文件描述符的flag中可能会有O_CLOSEXEC,这样子进程exec之后,对应的文件描述符就被关闭了。这样父子进程就会使用不冲突的文件描述符。
1.父子进程执行不同的代码段
2.子进程要执行不同的程序,可以通过fork之后调用exec来让新程序执行
vfork是为了简化fork之后调用exec的步骤。vfork保证了子进程先进行,直到子进程执行了exit或者exec。vfork实现中,子进程会借用父进程的地址空间,也就是说如果在子进程中改变一些值,会影响到父进程中的值。所以一般vfork是为了spawn用的,即fork-exec。
不管如何退出,kernel最终都会执行相同的代码段,即关闭文件描述符,是否内存空间等等。
在一个正常的退出中,是内核,而不是进程来保障终止状态。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
//两个函数返回值:若成功则返回进程ID,若出错则返回-1
#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函数族,主要用于在父进程fork之后,调用exec来执行另一个程序,新的程序从它自己的main函数出开始执行。调用exec并不会改变子进程的pID,exec函数仅仅改变了子进程的text,data, heap and stack。
说明:
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
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的优势在于他处理了所有的错误,并且所有的信号处理。
说明:
对于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。
int nice(int incr);
说明:
只有root权限的进程可以root来提高自己的优先级,其他权限的用户只能用来降低自己的优先级。
times函数获得进程的相关时间。注意times返回的就是墙上时钟时间,所以tms结构体中没有墙上时钟时间。
说明:
三种时间:
进程的三种状态为阻塞、就绪、运行。
时钟时间 = 阻塞时间 + 就绪时间 +运行时间
用户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调用后返回,但是这个时候栈空间被子进程重写了,他就找不到要回到哪里去了,有可能跑到别的地方去了。