C程序总是从main函数开始执行。main函数的原型是:
当内核执行C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。启动例程从内核取得命令行参数和环境变量值,然后调用main函数。
有8种方式能种子进程,其中5种是正常终止,3种是异常终止。
正常:
异常:
其中三个正常终止函数:_exit、_Exit和exit之间是有区别的。_exit、_Exit立即退出;而exit则先执行一些清理工作,然后再退出。
并且可以利用函数atexit来声明在exit退出前会被调用的函数(称为终止处理程序),atexit原型为:
如下图所示的一个C程序是如何启动和终止的:
注意:内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显示或隐式地(通过调用exit)滴啊用_exit或_Exit;进程也可非自愿地有 一个信号使其终止。
每个进程所分配的内存由很多部分组成,通常称之为"段"。如下所示:
Figure 1 典型的存储空间安排
每个进程都有进程ID,其中ID为0的进程是调度进程(交换进程);进程为1的是init进程,并且init进程决不会终止,
进程的创建可以通过函数fork和vfork进行。
重点注意:
不同的进程拥有不同的地址空间。如在不同的两个进程都拥有100的地址,那么这两个100存放的值是不一样的。
fork函数被调用一次,却返回两次,子进程返回0,父进程返回子进程的ID值;并且子进程的内存是父进程的完全副本,包括局部、全局、静态的数据空间、堆和栈的副本。其中有些系统对fork进行了优化,即写时复制(Copy-On-Write)技术,在fork调用之后父子进程共享同一个区域,只有当父进程或子进程的任意一方试图修改这些区域,内核才会为修改区域的那块内存制作一个副本。
1) 缓冲问题
在fork函数调用之前,需要考虑缓冲中是否有数据存在。是为不带缓冲的系统调用,还是带缓冲的标准I/O。
2) 文件共享
在fork调用之后,子进程复制了父进程文件描述符,并且子进程和父进程共享该文件的偏移量。若父子进程之间没有任何形式的同步,那么它们的输出就会相互混合。在fork之后处理文件描述符有以下两种使用模式:
子进程还继承父进程的属性还有:
3) fork使用模式
fork有两种用法,复制自己从而进行不同的操作,或是复制自己执行不同的程序。
vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序。所以vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。vfork保证子进程先运行,并在子进程调用exec或exit之前,它在父进程的空间中运行。
在1.1小节中,介绍了进程5种正常终止和3种异常终止的方式。其中不管进程如何终止,最后都会执行内核中的同一段代码,来为相应进程关闭所有打开描述符,及释放它所使用的存储器等。
子进程的通过如下方式来通知父进程自己是如何终止的:
在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
注意:
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止时一个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。其中父进程可以通过调用wait或waitpid来获得子进程的终止状态。
其中在调用上述两函数之后,可能会发生如下的情况:
这两个函数的区别是:
由于UNIX信号一般是不排队的,所以当有多个子进程同时终止,那么wait和waitpid只能获得一个子进程的终止状态,为了防止余下的子进程得不到处理(成为僵死进程),那么只能使用waitpid监控了。
上述代码如果将waitpid改为wait,也可以获得所有子进程的终止状态,但父进程(当前进程)将进入永远的阻塞状态,因为如果没有子进程,那么wait将会阻塞,不能返回。
Linux可以通过一系列exec函数(7个)来执行另一个程序,被加载的新程序将替换某一进程的内存空间,旧进程的栈、数据以及堆段会被新程序的相应部件所替换,并且新程序会从main()函数出开始执行。调用exec函数之后,进程的ID仍保持不变。
前4个函数取路径名作为参数,后两个取文件名作为参数(p),最后一个取文件描述符作为参数(f)。
第二个区别是:
是参数表传递的不同,l表示列表list,v表示矢量vector。
第三个区别是:
是否传递环境表e。
在Linux中只有execve是内核的系统调用,另外6个则只是库函数,6个库函数都需调用该系统调用,如下图所示:
1) 基本属性
在执行exec之后,除了进程ID不变外,新程序还继承了如下属性:
2) 文件描述符
其中打开文件的处理与每个描述符的执行时关闭(close-on-exec)标志值有关,即FD_CLOEXEC,若对文件描述符设置了此标志,则在执行exec时关闭该描述符;否则该描述符仍打开。
3) 设置用户ID和设置组ID
注意,在exec前后实际用户ID和实际组ID保持不变,但如果对pathname所指定的程序文件设置了set_user-ID(set-group-ID)权限位,那么系统调用会在执行此文件时将进程的有效用户(组)ID置为程序文件的属主(组)ID。
程序可通过调用system函数来执行任意的shell命令。其函数原型如下:
函数system()的实现中调用了fork、exec和waitpid等函数,其创建一个子进程来运行shell,从而执行了命令cmdstring。如system("ls | less")
信号是事件发生时对进程的一种通知方式。事件可以是硬件异常(如除以0)、软件条件(如alarm定时器超时)、终端产生的信号或调用kill函数。
进程可以选用"阻塞信号递送"。如果为进程产生了一个阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则该进程将此信号保持为未决状态,直到该进程对此信号解除了阻塞,或者将对此信号的动作更改为忽略。
内核将信号传递给进程有如下几种可能:
注意:
在某个信号出现时,可以告诉内核按下列3种方式之一进行处理:
大多数可以这样处理,但有两种信号不能被忽略,是SIGKILL和SIGSTOP。
用户可以通知内核在某种信号发生时,调用一个用户函数。但SIGKILL和SIGSTOP不能被捕捉。
每一种信号都有默认动作,基本都是终止该进程。
signal和sigaction函数都是设定接收信号的处理方式,若未对信号重新设置,则进程按默认方式处理。
UNIX系统信号机制最简单的接口是signal函数:
signo参数是要处理的信号名,func的值可以是常量SIG_IGN,常量SIG_DEL或当接到信号后要调用的函数地址。即对应信号的三种处理方式。
在exec或fork后的信号处理方式;
1) 调用exec
一个进程原先要捕捉的信号,当调用exec执行一个新程序后,这些被捕获信号的处理方式都被更改为按系统默认方式处理,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义。
2) fork进程
当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映象,所以信号捕捉函数的地址在子进程中是有意义的。
sigaction函数的功能是检测或修改与制定信号相关联的处理动作。
使用方法:
当更改信号动作时,如果sa_handler字段包含一个信号捕捉函数的地址(不是常量SIG_IGN或SIG_DFL),则sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。
kill函数将信号发送给指定进程或进程组;raise函数是立即向进程自身发送信号;alarm函数是设置一个定时器,当超时后向自身发送一个信号。
注意:将信号发生给其它进程需要权限
当进程调用raise函数,信号立即被传递(即,在raise函数返回调用者之前)。并且raise出错的唯一原因是signo无效。
使用alarm函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,会产生SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程。其中进程调用alam()函数后是不挂起的,仍然继续执行。
sleep和pause函数都能使调用进程挂起,但sleep在指定的时间超时或接收到信号时就能唤醒;而pause只能接收到信号才得以唤醒,否则将一直处于挂起状态。
此函数使调用进程被挂起知道满足以下两个条件之一:
pause函数使得调用进程自己挂起,直至当前进程接收到某种信号,才得以唤醒;否则一直处于阻塞状态。
注意:
只有执行了一个信号处理程序并从其返回时,pause才返回。
内核会为每个进程维护一个信号掩码(即一个信号集),从而进程将屏蔽信号掩码中的信号集。若将被屏蔽的信号发送给进程,那么该信号的传递将被延后(只是被延后,不是被删除),直至该进程解除该信号,从而该进程仍能够接收到先前被屏蔽的信号。
关于进程的信号屏蔽,UNIX还提供了如下功能:
l sigprocmask
调用函数sigprocmask可以检测或更改,或同时进行检测和更改进程的信号屏蔽字。
l sigpending
该函数返回正处于等待状态的信号集,即由于某个信号是屏蔽字段的类型之一,并且发送给了调用进程,那么将从set返回。
l sigsuspend(sigset_t *sigmask)
该函数的功能是将屏蔽字更改为sigmask,并挂起调用进程;直至接收到sigmask之外的信号,才唤醒该进程,并将屏蔽字恢复为调用sigsuspend函数之前的屏蔽字。该过程可分为3个步骤完成:首先设置指定屏障字;然后使进程挂起(类似pause功能);最后接收到除sigmask信号,并恢复之前的屏蔽字。
注意:
屏蔽信号,并不是删除信号,当解除屏蔽信号时,被阻塞的信号仍能被进程接收。
管道有两种局限性:
创建管道函数原型是:
经由参数fd返回两个文件描述符,fd[0]为读而打开,fd[1]为写而打开,然后就能像使用普通文件描述符一样,进行读写了。
单个进程中的观点几乎没有任何用处。通常,进程会先调用pipe,接着调用fork,从而创建从父进程到子进程的IPC通道。对于从父进程到子进程的管道,如父进程关闭管道的读端(fd[0]),子进程关闭写端(fd[1])。如下图所示:
当管道的一端被关闭后,下列两条规则起作用:
如下程序:
FIFO也称为命名管道。与管道不同,通过FIFO不相关的进程也能交换数据。FIFO的使用方式是:先创建(mkfifo),然后使用open打开,接着就能使用I/O系统调用(如read()、write()和close())操作打开的文件描述符了。
与管道一样,FIFO也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。
其中需注意:
消息队列与FIFO类似,都是需要先创建一个标识,然后通过这个标识进行消息的发送和接收。
1) 创建或打开一个消息队列
2) 发送消息
msgsnd功能是将新消息添加到队列尾端。
3) 接收消息
msgrcv用于从队列中取消息,可以按先进先出次序取消息,也可以按消息的类型字段取消息。
信号量不是用来在进程间传输数据的,相反,它是用来同步进程的动作。信号量的一个常见用途是同步对一块共享内存的访问以防止出现一个进程在访问共享内存的同时另一个进程更新这块内存的情况。
为了获得共享资源,进程需要执行下列操作:
使用信号量的常规步骤如下:
共享存储允许多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种IPC。为了防止多个进程同时访问共享存储区,通常使用信号量同步共享存储区的访问(也可使用记录锁或互斥量)。
共享存储与内存映射不同之处在于,前者没有相关的文件,共享存储段是内存的匿名段。
为使用一个共享内存段通常需要执行下面的步骤:
1) 什么是线程
线程是一个进程内部的一个控制序列。所有的进程都至少有一个执行线程。
一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码,程序的全局内存和堆内存、栈以及文件描述符,即同一个进程的线程同处于一个地址空间,彼此能够互相访问栈地址(只要可见)。但不同的进程拥有完成不相干的地址空间。
如:
2) 线程标识
每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
在POSIX线程(pthread)情况下,程序开始运行时,它也是以单进程中的单个控制线程(主线程)启动的。
当创建成功返回时,新创建线程的线程ID会被设置成tidp指向的内存单元;新创建的线程从start_rtn(称为启动例程)函数的地址开始运行,该函数只有一个不类型指针参数art。
如果进程中的任意线程调用了exit、_Exit或者_exit,那么整个进程就会终止,其中主线程退出也会使整个进程终止。与此类似,如果信号的默认动作是终止进程,那么,发送到线程的信号就会终止整个进程。
单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流:
pthread_exit是线程退出函数,rval_ptr是推出的状态码;
调用pthead_join线程将获得指定线程的退出状态;当线程调用了pthread_join函数则将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。
线程可以通过调用pthrad_cancel函数来请求取消同一个进程中其他线程。在默认情况下,pthread_cancel函数会使得tid标识的线程的行为表现为如同调用了参数为PTHREAD_CANCEL的pthread_exit函数,但是,线程可以忽略取消或者控制如何被取消。
线程内存空间中存在着一个特殊栈,此栈用来记录清理函数的地址。该栈由如下两个函数来完成入栈和出栈操作。它们的执行顺序与它们的注册时相反。
当线程执行以下动作时,清理函数rtn将被调用执行,其中arg是传递的函数。
术语临界区(critical section)是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,亦即,同时访问同一共享资源的其他线程不应中断该片段的执行。
线程有如下的5种基本的同步机制。
互斥量(mutex)是线程同步的一把锁,该锁有两个状态:已锁定和未锁定。互斥量保证了同一个时间只有一个线程(获得锁的进程)访问临界区,并且该锁只能由获得锁的线程主动释放。
1) 初始化与销毁
互斥量的数据类型是pthread_mutex_t。在使用互斥量之前,首先必须对它进行初始化,有如下两种情况:
如果是动态分配的互斥量 (例如,通过调用malloc函数或全局变量),在释放内存前需要调用pthread_mutex_destroy。
要默认的属性初始化互斥量,只需把attr设为NULL。
2) 加锁与解锁
对互斥量进行加锁,需要调用pthread_mutex_lock,如果互斥量已经上锁了,调用线程将阻塞知道互斥量被解锁;对互斥量解锁,需要调用pthread_mutex_unlock。
如果线程不希望被阻塞,可以使用pthread_mutex_trylock尝试对互斥量进行加锁。调用pthread_mutex_trylock有两种情况:
互斥量为防止多个线程同时访问同一个共享量,而条件变量允许一个线程就某个共享变量(或其他共享资源)的状态变化通知其他线程,并让其他线程等待(阻塞于)这一通知。条件变量总是结合互斥量一起使用,条件变量就共享变量的状态改变发出通知,而互斥量则提供对该共享变量访问的互斥。
1) 初始化与销毁
与互斥量类似,对条件变量的初始化分为静态类型和动态类型,动态类型的初始化和销毁函数为:
2) 等待通知
等待其他线程的发送的条件变量,可以使用如下两个函数,第二个只是增加了等待时间,其余功能都一样。
调用线程将锁住的mutex信号量传递给pthread_cond_wait函数,然后该线程自动进入等待状态(阻塞),并对此互斥量解锁。从pthread_cond_wait唤醒必须同时具备如下条件:
3) 发送通知
这两个函数都可以用于唤醒阻塞于cond条件变量的线程,pthread_cond_signal函数至少能唤醒一个等待该条件变量的线程,而pthread_cond_broadcast函数则能唤醒等待该条件变量的所有线程。
注意:
与只有2种状态的互斥量不同,读写锁有3种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。其中一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
在读写锁的3种状态下,进行读或写申请给出的响应各不相同:
1) 初始化与销毁
与互斥量相比,读写锁在使用之前必须初始化在释放内存之间必须销毁。
2) 加锁与解锁
对读写锁进行读加锁、写加锁,以及解锁的3个函数如下:
Single UNIX Specification还定义了读写锁原语的条件版本
自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁可用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。
自选锁通常作为底层原语用于实现其他类型的锁。但是,在用户层,自旋锁并不是非常有用,除非运行在不允许抢占的实时调度类中。所以这里不过多叙述.
屏障是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有合作线程都到达某一点,然后从该点继续执行。
1) 初始化与销毁
与读写锁类似,屏障不分静态和动态变量,所有屏障类型变量都需通过pthread_barrier_init进行初始化。
初始化屏障时,可以使用count参数指定,在允许所有线程继续运行之前,必须到达屏障的线程数目。
2)等待
可以使用pthread_barrier_wait函数来表明,线程已完成工作,准备等所有其他线程赶上来。
调用pthread_barrier_wait的线程在屏障计数(调用pthread_barrier_init时设定)未满足条件时,会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait的线程,就满足了屏障计数,所有线程都被唤醒。
当进程执行fork和exec会促使子进程继承父进程很多属性,下表对fork和exec执行后对进程属性的影响进行的对比和总结。
进程属性 |
exec() |
fork() |
影响属性的接口;额外说明 |
进程地址空间 |
|||
文本段 |
û |
共享 |
子进程与父进程共享文本段 |
栈段 |
û |
ü |
函数入口/出口:alloca()、longjmp()、siglongjmp() |
数据段和堆段 |
见注释 |
ü |
Brk()、sbrk() |
环境变量 |
û |
ü |
putenv()、setenv();直接修改environ。execle()和execve()队对其改进,其它exec()调用则会加以保护。 |
内存映射 |
û |
ü;见注释 |
mmap()、munmap()。跨越fork()进程,映射的MAP_NORESERVE标志得以继承。带有madvise(MADV_DONTFORK)标志的映射则不会跨fork()继承 |
内存锁 |
û |
û |
mlock()、munlock() |
进程标识符和凭证 |
|||
进程ID |
ü |
û |
|
父进程ID |
ü |
û |
|
进程组ID |
ü |
ü |
setpgid() |
回话ID |
ü |
ü |
setsid() |
实际ID |
ü |
ü |
setuid()、setgid(),以及相关调用 |
有效和保存设置ID |
见注释 |
ü |
Setuid()、setgid(),以及相关调用 |
补充组ID |
ü |
ü |
setgroups()、initgroups() |
文件、文件IO和目录 |
|||
打开文件描述符 |
见注释 |
ü |
open(),close()、dup()、pipe()、socket()等。文件描述符在跨越exec()调用的过程中得以保存,除非对其设置了执行时关闭(close-on-exec)标志。父、子进程中的描述符指向相同的打开文件描述符 |
执行时关闭(close-on-exec)标志 |
ü (如果关闭) |
ü |
fcntl(F_SETFD) |
文件偏移 |
ü |
共用 |
lseek()、read()、write()、readv()、writev()。父、子进程共享文件偏移 |
打开文件状态标志 |
ü |
共用 |
Open()、fcntl(F_SETFL)。父子进程共享打开文件状态标志 |
异步IO操作 |
见注释 |
û |
aio_read()、aio_write()以及相关调用。调用exec()期间,会取消尚未完成的操作 |
目录流 |
û |
ü (见注释) |
opendir()、readdir()。SUSv3规定,子级才能获得父进程目录流的一份副本,不过这些副本可以(也可以不)共享目录流的位置。Linux系统不共享目录流的位置 |
文件系统 |
|||
当前工作目录 |
ü |
ü |
chdir() |
根目录 |
ü |
ü |
chroot() |
文件模型创建掩码 |
ü |
ü |
umask() |
信号 |
|||
信号设置 |
见注释 |
ü |
Signal()、sigaction()。将处置设置成默认或忽略的信号在执行exec()期间保持不变;已捕获的信号会恢复为默认处置 |
信号掩码 |
ü |
ü |
信号传递;sigprocmask()、sigaction() |
挂起(pending)信号集合 |
ü |
û |
信号传递;raise()、kill()、sigqueue() |
备选信号栈 |
û |
ü |
sigaltstack() |
定时器 |
|||
间隔定时器 |
ü |
û |
setitimer() |
由alarm()设置的定时器 |
ü |
û |
alarm() |
POSIX定时器 |
û |
û |
timer_create()及其相关调用 |
POSIX线程 |
|||
线程 |
û |
见注释 |
fork()调用期间,子进程只会复制调用线程 |
线程可撤销状态与类型 |
û |
ü |
Exec()之后,将可撤销类型和状态分别重置为PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DEFERRED |
互斥量与条件变量 |
û |
ü |
关于调用fork()期间对互斥量 |
优先级与调度 |
|||
nice值 |
ü |
ü |
nice()、setpriority() |
调度策略及优先级 |
ü |
ü |
sched_setcheduler()、sched_setparam() |
资源与CPU时间 |
|||
资源限制 |
ü |
û |
setrlimit() |
进程和子进程的CPU时间 |
ü |
û |
由times()返回 |
资源使用量 |
ü |
û |
由getrusage()返回 |
进程间通信 |
|||
System V共享内存段 |
û |
ü |
shmat()、shemdt() |
POSIX共享内存段 |
û |
ü |
shm_open()及相关调用 |
POSIX消息队列 |
û |
ü |
mq_open()及相关调用。父子进程的描述符都指向同一打开消息队列描述。子进程并不继承父进程的消息通知注册消息 |
POSIX命名信号量 |
û |
共用 |
sem_open()及其相关调用。子进程与父进程共享对相同信号量的引用 |
POSIX未命名信号量 |
û |
见注释 |
sem_init()及其相关调用。如果信号量位于共享内存区域,那么子进程与父进程共享信号量;否则,子进程拥有属于自己的信号量拷贝。 |
System V信号量调整 |
ü |
û |
|
文件锁 |
ü |
见注释 |
flock()。子进程自父进程处继承对同一锁的引用 |
记录锁 |
见注释 |
û |
fcntl(F_SETLK)。除非将指代文件的文件描述符标记为执行时关闭,否则会跨越exec()对锁加以保护 |
杂项 |
|||
地区设置 |
û |
ü |
setlocale()。作为C运行时初始化的一部分,执行新程序后会调用setlocale(LC_ALL,"C")的等效函数 |
浮点环境 |
û |
ü |
运行新程序时,将浮点环境状态重置为默认值,参考fenv |
控制终端 |
ü |
ü |
|
退出处理器程序 |
û |
ü |
atexit()、on_exit() |
Linux特有 |
|||
文件系统ID |
见注释 |
ü |
setfsuid()、setfsgid()。一旦相应的有效ID发生变化,那么这些ID也会随之改变 |
timeerfd定时器 |
ü |
见注释 |
timerfd_create(),子进程继承的文件描述符与父进程指向相同的定时器 |
能力 |
见注释 |
ü |
capset()。 |
功能外延集合 |
ü |
ü |
|
能力安全位(securebits)标志 |
见注释 |
ü |
执行exec()期间,会保全所有的安全位标志,SECBIT_KEEP_CAPS除外,总是会清除该标志 |
CPU黏性(affinity) |
ü |
ü |
sched_setaffinity() |
SCHED_RESET_ON_FORK |
ü |
ü |
|
允许的CPU |
ü |
ü |
|
允许的内存节点 |
ü |
ü |
|
内存策略 |
ü |
ü |
|
文件租约 |
ü |
ü |
Fcntl(F_SETLEASE)。子进程从父进程处继承对相同租约的引用 |
目录变更通知 |
ü |
û |
dnotify API,通过fcntl(F_NOTIFY)来实现支持 |
prctl(PR_SET_DUMPABLE) |
见注释 |
ü |
exec()执行期间会设置PR_SET_DUMPABLE标志,自行设置用户或组ID程序的情况除外,此时将清除该标志 |
prctl(PR_SET_PDEATHIG) |
ü |
û |
|
prctl(PR_SET_NAME) |
û |
ü |
|
oom_adj |
ü |
ü |
|
coredump_filter |
ü |
ü |