7进程环境
7.2 内核执行C程序时,在调用main前先调用一个启动例程。编译器调用链接器将此例程指定为程序起始地址,这个启动例程从内核取得命令行参数和环境变量值,然后调用main。
7.3 进程终止的8种方式:
正常的5种:从main返回;调用exit;调用_exit和_Exit;最后一个线程从其启动例程返回;最后一个线程调用pthread_exit;
异常的3种:调用abort,接到一个信号终止,最后一个线程对取消请求做出回应。
exit函数会执行一系列的清理工作,然后才会进入内核,而_exit和_Exit则会直接进入内核。例如当在程序中使用atexit系统调用注册了清理函数,在使用exit退出时,注册的函数将会按照顺序依次执行;而使用_exit _Exit函数退出时,注册的函数将不会执行。
7.6 C程序的存储布局空间
课后习题中有一个与本节相关的,即 为什么有些系统不允许程序访问自己的低地址0,由此图可得知。
7.8存储器分配的三个函数 malloc calloc(此函数分配空间后空间中每一位被初始化为0),realloc。
这三个函数返回的指针一定是适当对齐的以使其用于任何数据对象。要满足最大的数据类型的对齐。比如存在int,char,double等数据时,指针的值要按照对齐要求最多的数据类型double来确定为8的倍数。
这三个alloc程序返回void*,如果程序中包含了#include<stdlib.h>就无需再进行强制类型转换。
这三个函数使用sbrk调用实现,sbrk可以扩充或缩小进程的存储空间,但是大多数malloc和free都不减小进程空间,通常将free掉的内存继续放在进程的malloc池中而不返回给内核。
大多数malloc分配的空间要比要求的更大一点,因为要记录管理信息,该块的长度,下一个块地址等等。
7.10 setjmp和longjmp与goto函数的区别,前者跨越函数(在多个函数间)进行跳转,而goto只能在一个函数中进行跳转。
当调用跳转时,自动、寄存器变量和易失变量的变化较为复杂,书中例子如下
static int globval;
1: int main()
2: {
3: int autoval;4: register int regival;5: volatile int volaval;6: static int statval;7:
8: globval=1;autoval=2;regival=3;volaval=4;statval=5;
9:
10: if(setjmp(jmpbuffer) != 0)11: {
12: printf("after longjmp:\n");13: printf("globval = %d,autoval = %d,regival = %d,volaval = %d,statval = %d\n14: globval,autoval,regival,volaval,statval");
15: exit(0);
16: }
17:
18: globval=95;autoval=96;regival=97;volaval=98;statval=99;
19: printf("before longjmp:\n");20: printf("globval = %d,autoval = %d,regival = %d,volaval = %d,statval = %d\n21: globval,autoval,regival,volaval,statval");
22: longjmp(jmpbuffer);
23: }
运行结果:
gcc testjmp.c 然后运行,两次输出结果相同,就是说跳转以后,程序从内存中重新读取了数据;
如果gcc –O testjmp.c 然后运行,静态变量和vovatile变量数据为90+的数据,而自动和局部变量都是跳转以前的值。因为进行了优化编译后,编译器会优先采用寄存器中的值而不是读取存储器,所以我们未加vovatile修饰的局部变量就会读取寄存器中的值。而longjmp跳转后,寄存器中的值会被恢复为setjmp时候的值,所以我们打印出的自动和局部变量是原来的小数字,而静态和vovatile数据会重新读取存储器获得,就是大数字。
关于自动变量还有一个潜在问题,就是声明自动变量的函数返回后,该自动变量由于退栈就已经不存在了,在这之后不能引用该自动变量。
8进程控制
8.2fork函数
由fork创建子进程,子进程是父进程的副本,获得父进程数据空间、堆和栈的副本,父子进程共享正文段。由于fork后经常跟随者exec调用,所以现在很多实现是这样的,在fork后并不复制父进程的数据段、堆栈;采用写时复制,即这些区域由父子共享,且这些区域为只读;当父子进程有一个试图改变这些区域,内核将为修改区域那块内存制作一个副本。父子进程共享同一张文件表,这意味着父子进程对同一个文件共享文件偏移量,父进程更改了文件偏移量,子进程也会更新。
8.4vfork函数
vfork创建一个子进程,子进程会马上调用exec或exit,所以vfork出来的子进程并不复制父进程的地址空间,在执行exec和exit之前,它在父进程地址空间中运行。
8.5
僵死进程:一个已经终止但是其父进程并未对其进行善后处理(获取终止子进程的有关信息,释放其占用资源)的进程。
父进程如何获取子进程状态:内核为每个终止子进程保存一定信息,父进程调用wait或者waitpid时就可以得到这些信息。
如果子进程还未结束,父进程就已经挂掉,那么这些孤儿进程的父进程被设置为init进程。init进程永远不会结束,那么在init收养的某个进程结束后,init就会调用一个wait函数来取得其终止状态,并进行处理,防止这些进程成为僵死进程。
8.11更改用户ID和组ID
在UNIX系统中,特权是基于用户和组ID的,当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们需要更换自己的用户ID或者组ID,使得新ID具有合适的特权或访问权限。同时,当程序需要降低其特权或阻止对某些资源的访问时,也需要更换用户ID和组ID。
改变用户ID和组ID有两种途径:1,exec执行程序时,根据程序文件的设置用户ID位是否打开,来更改,有效用户ID位为程序文件拥有者,实际用户ID为运行此程序的用户。如果设置用户ID位未打开,则不能改变。
2,setuid函数,当运行程序的进程有超级用户权限时候,setuid(uid)将实际用户ID有效用户ID保存的设置用户ID设置为uid;运行程序没有超级用户权限,但是uid=实际用户ID或者设置用户ID,那么僵有效用户ID设置为uid;如果不是上面两种情况,那么setuid则会设置errno返回。
书中使用man程序对这个过程进行解释。
10 信号
10.2 处理信号的三种方式:1,忽略;2,捕捉;3执行系统默认动作。
两个常量的意义:SIG_IGN代表内核忽略信号,SIG_DFL表示采用系统默认动作。
10.5 中断的系统调用
如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。返回出错值,将errno设置为EINTR。
10.6可重入函数
进程捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理程序中断,执行完信号处理程序后,返回到中断处再继续执行被中断的指令序列。但是有些函数被中断后返回可能会造成问题,这些函数就是不可重入的,相反中断后返回继续执行无问题的函数即称为可重入的函数。
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
10.11信号相关的一些函数和结构
sigset_t 信号集;sigprocmask(int,const sigset_t *,sigset_t *)更改信号屏蔽字的函数;sigaction(int,const struct sigaction*,struct sigaction* ),修改与指定与信号相关联的处理动作;sigsuspend,pause,sigwait这几个函数可用于实现等待某个信号;
11线程
11.2线程概念
线程的一些好处:
通过为每种事件类型的处理分配单独的线程,能够简化处理异步事件的代码;
多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述符的共享,多个线程自动地可以访问相同的存储地址空间和文件描述符;
多线程可以提升程序的并行性;
11.5线程终止
终止的三种方式:1,调用pthread_exit;2从启动例程中返回;3被同一进程中其他线程取消(通过调用pthread_cancle);
前两种方式退出后,进程中其他线程可以通过调用pthread_join函数获得该线程的退出状态。
线程可以安排自己退出时需要调用的函数来处理后事,类似于进程的atexit注册函数,线程也有自己的函数pthread_cleanup_push和pthread_cleanup_pop。
11.6线程同步
多个线程共享相同的内存时,需要确保每个线程看到一致的视图。如果共享的变量是只读的则对此变量的访问无需同步,但是当变量非只读时就必须考虑同步。
互斥量,读写锁,条件变量是常用的实现同步的工具。
互斥量即pthread_mutex_t数据类型,通过使用pthread_mutex_init初始化,pthread_mutex_lock和pthread_mutex_unlock来加解锁。要避免死锁问题,同一个执行流中对一个互斥量连续加锁两次会死锁,两个线程使用两个互斥量的情况下要确保线程加锁顺序相同否则也会死锁。
读写锁允许更高的并行,一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定条件的发生。
12线程控制
12.5重入
如果一个函数在同一时刻可以被多个线程安全调用,就称该函数式线程安全的。如果一个函数对多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能够说明对信号处理程序来说该函数也是可以重入的。如果函数对信号处理程序的重入是安全的,那么就说函数是异步-信号安全的。
12.6 线程私有数据
errno是线程私有化数据。
12.8线程和信号
每个线程都有自己的信号屏蔽字,但是信号的处理程序是进程中所有线程共享的。
进程中的信号时递送到单个线程去的,如果信号与硬件故障或者计时器超时有关,该信号就被发送到引起事件的线程去,而其他信号则被发送到任意一个线程。
线程可以通过调用sigwait等待一个或者多个信号的发生。使用sigwait的好处是可以简化信号处理,为了防止信号中断线程,把信号加入到每个线程的信号屏蔽字中,然后安排专用线程做信号处理,这些专用线程可以进行函数调用,不需要担心在信号处理程序中调用哪些函数是安全的,因为这些函数调用来自正常线程环境。
12.9线程和fork
在子进程内部只存在一个线程,它是由父进程中调用fork的线程的副本构成的。
14高级I/O
非阻塞I/O,对一个给定的描述符有两种方法对其指定非阻塞I/O,1,如果调用open获得描述符,可以指定O_NOBLOCK标志;2,对于已经打开的描述符,可以调用fcntl打开O_NOBLOCK文件状态标志。
记录锁:当一个进程在读或者修改文件的某个部分时,它可以阻止其他进程修改同一文件区。
对早期的UNIX系统的一种批评是它们不能用来运行数据库系统,因为这些系统不支持部分的对文件加锁。现在可以通过使用fcntl函数来实现记录锁功能。
int fcntl(int filedes,int cmd,…)。对于记录锁,cmd是F_GETLK、F_SETLK、F_SETLKW。第三个参数是一个纸箱flock结构的指针。
I/O多路转接:当从一个描述符读,然后又写到另一个描述符时,可以在下列形式的循环中使用阻塞I/O:
1: while((n = read(STDIN_FILENO,buf,BUFSIZ)) > 0)2: if(write(STDOUT_FILENO,buf,n) != n)3: err_sys("write error");
这种形式的I/O很常见,但是如果必须从两个描述符读,如果使用阻塞I/O,那么可能长时间阻塞在一个描述符上。select和poll函数可以用来面对这个问题。
select等待设定的描述符集中哪个可用,当有一个可用时select就返回。
select在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect、accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。