Linux进程管理命令——进程查看
- ps命令:报告进程标识、用户、CPU时间消耗及其他属性
- 命令单独使用可以看到前台执行的进程;后台进程可以使用带参 数的ps命令(如ps -ax)
- 提供进程的一次性查看,结果不连续
- 结果数据很精确,但数据量庞大
- top命令:显示CPU占用率为前几位的进程
- 动态显示,输出结果连续
- 消耗较多的系统资源
- pstree命令:列出当前的进程,以及它们的树状结构
- 将当前的执行程序以树状结构显示,弥补ps命令的不足
- 支持指定特定程序(PID)或使用者(USER)作为显示的起始
Linux进程管理命令—进程终止
- 终止一个进程或终止一个正在运行的程序
- kill命令:根据PID向进程发送信号,缺省操作是停止进程
- 如果进程启动了子进程,只终止父进程,子进程运行中将仍 消耗资源成为“僵尸”进程,可用kill -9强制终止退出
- pkill命令:终止同一进程组内的所有进程。允许指定要终止的进程名称,而非PID
- Killall命令:与pkill应用方法类似,直接杀死运行中的程 序
- 数据库服务器的父进程不能用这些命令杀死(容易产生更多 的文件碎片导致数据库崩溃)
Linux进程控制函数——进程创建
fork()
pid=fork();
fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。
它不需要参数并返回一个整数值。下面是fork()返回的不同值。
- 负值:创建子进程失败。
- 零:返回到新创建的子进程。
- 正值:返回父母或来电者。该值包含新创建的子进程的进程ID
头文件:1
2
函数原型:
pid_t fork( void);
(pid_t 是一个宏定义,其实质是int 被定义在#include
返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1
函数说明:
一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。
为什么fork会返回两次?
由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
(1)在父进程中,fork返回新创建子进程的进程ID;
(2)在子进程中,fork返回0;
(3)如果出现错误,fork返回一个负值。
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
引用一位网友的话来解释fork函数返回的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fork函数返回的值指向子进程的进程id, 因为子进程没有子进程,所以其fork函数返回的值为0.
调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回,箭头表示各自的执行处。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。
exec()
函数族exec() :启动另外的进程取代当前的进程
include
- extern char **environ;
- int execl(const char *path, const char *arg, …);
- int execlp(const char *file, const char *arg, …);
- int execle(const char *path, const char *arg, const char *envp[]);
- int execv(const char *path, const char *argv[]);
- int execve(const char *path, const char *argv[], const char *envp[];
- int execvp(const char *file, const char *argv[]);
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
exec族函数的作用
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似”三十六计”中的”金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
现在我们应该明白了,Linux下是如何执行新程序的,每当有进程认为自己不能为系统和用户做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
事实上第二种情况被应用得如此普遍,以至于Linux专门为其作了优化,我们已经知道,fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种”写时拷贝(copy-on-write)”技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白作无用功了,也就提高了效率。
对于新程序的命令行参数和环境表有长度大小的限制,对于linux来讲这个限制是4096个字节。执行了exec函数的进程不改变以下进程特征:
- 1.进程ID和父进程ID
- 2.实际用户ID和实际组ID
- 3.进程组ID和附加组ID
- 4.控制终端
- 5.会话ID
- 6.时钟预留着时间
- 7.当前工作目录和根目录
- 8.文件创建屏蔽字和文件锁
- 9.信号屏蔽字和未处理信号集
- 10.资源限制
返回值
如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno 中。
注意
大家在平时的编程中,如果用到了exec函数族,一定记得要加错误判断语句。因为与其他系统调用比起来,exec很容易受伤,被执行文件的位置,权限等很多因素都能导致该调用的失败。最常见的错误是:
- 1.找不到文件或路径,此时errno被设置为ENOENT;
- 2.数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;
- 3.没有对要执行文件的运行权限,此时errno被设置为EACCES。
- l表示以参数列表的形式调用
- v表示以参数数组的方式调用
- e表示可传递环境变量
- p表示PATH中搜索执行的文件,如果给出的不是绝对路径就会去PATH搜索相应名字的文件,如PATH没有设置, 则会默认在/bin,/usr/bin下搜索。
- 另:调用时参数必须以NULL结束。原进程打开的文件描述符是不会在exec中关闭的,除非用fcntl设置它们的“执行时关闭标志(close on exec)”而原进程打开的目录流都将在新进程中关闭。
Linux进程属性操作
- 设置进程属性
- nice():改变进程执行的优先级
- setpgid():将指定进程的组进程设为指定的组识别码
- setpgrp():将目前进程的组进程识别码设为目前进程的进程 识别码,等价于setpgid(0,0)
- setpriority():设置进程、进程组和用户的执行优先权
- 获取进程属性
- getpid():获取目前进程的进程标识
- getpgid():获得参数pid指定进程所属的组识别码
- getpgrp():获得目前进程所属的组识别号,等价于
- getpgid(0)
- getpriotity():获得进程、进程组和用户的执行优先权
进程退出
- 正常退出:在main()函数中执行return、调用exit()函数 或_exit()函数
- 异常退出:调用abort()函数、进程收到信号而终止
- 区别
- exit是一个函数,有参数,把控制权交给系统
- return是函数执行完后的返回,将控制权交给调用函数
- exit是正常终止进程,abort是异常终止
- exit中参数为0代表进程正常终止,为其他值表示程序执行过程 中有错误发生
- exit()在头文件stdlib.h中声明,先执行清除操作,再将控制权 返回给内核
- _exit()在头文件unistd.h中声明,执行后立即返回给内核
等待进程终止
wait(); waitpid();
- ① wait() 语法格式: pid=wait(stat_addr);
wait()函数使父进程暂停执行,直到它的一个子进程结束为止,该函数 的返回值是终止运行的子进程的PID。参数status所指向的变量存放子 进程的退出码,即从子进程的main函数返回的值或子进程中exit()函数 的参数。如果status不是一个空指针,状态信息将被写入它指向的变量。 ② waitpid() 语法格式:waitpid(pid_t pid,int * status,int options)
用来等待子进程的结束,但它用于等待某个特定进程结束。
参数pid指明要等待的子进程的PID,参数status的含义与wait()函数中的 status相同。如果在调用 waitpid()时子进程已经结束,则 waitpid()会立即返回子进程结束状态值。 子进程的结束状态值会由参数 status 返回,而子进程的进程识别码也会一起返回。如果不在意结束状态值,则参数 status 可以设成 NULL。参数 pid 为欲等待的子进程识别码.
其他数值意义如下:- pid<-1 等待进程组识别码为 pid 绝对值的任何子进程。
- pid=-1 等待任何子进程,相当于 wait()。
- pid=0 等待进程组识别码与目前进程相同的任何子进程。
- pid>0 等待任何子进程识别码为 pid 的子进程。
参数options提供了一些额外的选项来控制waitpid,参数 option 可以为 0 或可以用”|”运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
WUNTRACED 若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。WIFSTOPPED(status)宏确定返回值是否对应与一个暂停子进程。子进程的结束状态返回后存于 status,底下有几个宏可判别结束情况:
WIFEXITED(status)如果若为正常结束子进程返回的状态,则为真;对于这种情况可执行WEXITSTATUS(status),取子进程传给exit或_eixt的低8位。
WEXITSTATUS(status)取得子进程 exit()返回的结束代码,一般会先用 WIFEXITED 来判断是否正常结束才能使用此宏。
WIFSIGNALED(status)若为异常结束子进程返回的状态,则为真;对于这种情况可执行WTERMSIG(status),取使子进程结束的信号编号。
WTERMSIG(status) 取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED 来判断后才使用此宏。
WIFSTOPPED(status) 若为当前暂停子进程返回的状态,则为真;对于这种情况可执行WSTOPSIG(status),取使子进程暂停的信号编号。
WSTOPSIG(status) 取得引发子进程暂停的信号代码,一般会先用 WIFSTOPPED 来判断后才使用此宏。
如果执行成功则返回子进程识别码(PID) ,如果有错误发生则返回
返回值-1。失败原因存于 errno 中。
进程的软中断通信
signal()
表头文件#include
功能:设置某一信号的对应动作
函数原型:void (*signal(int signum,void(* handler)(int)))(int);
或者:typedef void (*sig_t)( int );
sig_t signal(int signum,sig_t handler);
参数说明:
第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler描述了与信号关联的动作,它可以取以下三种值:
- (1)一个无返回值的函数地址
此函数必须在signal()被调用前申明,handler中为这个函数的名字。当接收到一个类型为signum的信号时,就执行handler 所指定的函数。这个函数应有如下形式的定义:
void func(int sig); - (2)SIG_IGN
这个符号表示忽略该信号,执行了相应的signal()调用后,进程会忽略类型为sig的信号。 - (3)SIG_DFL
这个符号表示恢复系统对信号的默认处理。
函数说明:
signal()会依参数signum 指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数handler指定的函数执行。当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数。但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中断。
返回值:返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。
下面的情况可以产生Signal:
- 按下CTRL+C产生SIGINT
- 硬件中断,如除0,非法内存访问(SIGSEV)等等
- Kill函数可以对进程发送Signal
- Kill命令。实际上是对Kill函数的一个包装
- 软件中断。如当Alarm Clock超时(SIGURG),当Reader中止之后又向管道写数据(SIGPIPE),等等
Signals:
Signal | Description |
---|---|
SIGABRT | 由调用abort函数产生,进程非正常退出 |
SIGALRM | 用alarm函数设置的timer超时或setitimer函数设置的interval timer超时 |
SIGBUS | 某种特定的硬件异常,通常由内存访问引起 |
SIGCANCEL | 由Solaris Thread Library内部使用,通常不会使用 |
SIGCHLD | 进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略 |
SIGCONT | 当被stop的进程恢复运行的时候,自动发送 |
SIGEMT | 和实现相关的硬件异常 |
SIGFPE | 数学相关的异常,如被0除,浮点溢出,等等 |
SIGFREEZE | Solaris专用,Hiberate或者Suspended时候发送 |
SIGHUP | 发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送 |
SIGILL | 非法指令异常 |
SIGINFO | BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程 |
SIGINT | 由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程 |
SIGIO | 异步IO事件 |
SIGIOT | 实现相关的硬件异常,一般对应SIGABRT |
SIGKILL | 无法处理和忽略。中止某个进程 |
SIGLWP | 由Solaris Thread Libray内部使用 |
SIGPIPE | 在reader中止之后写Pipe的时候发送 |
SIGPOLL | 当某个事件发送给Pollable Device的时候发送 |
SIGPROF | Setitimer指定的Profiling Interval Timer所产生 |
SIGPWR | 和系统相关。和UPS相关。 |
SIGQUIT | 输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程 |
SIGSEGV | 非法内存访问 |
SIGSTKFLT | Linux专用,数学协处理器的栈异常 |
SIGSTOP | 中止进程。无法处理和忽略。 |
SIGSYS | 非法系统调用 |
SIGTERM | 请求中止进程,kill命令缺省发送 |
SIGTHAW | Solaris专用,从Suspend恢复时候发送 |
SIGTRAP | 实现相关的硬件异常。一般是调试异常 |
SIGTSTP | Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程 |
SIGTTIN | 当Background Group的进程尝试读取Terminal的时候发送 |
SIGTTOU | 当Background Group的进程尝试写Terminal的时候发送 |
SIGURG | 当out-of-band data接收的时候可能发送 |
SIGUSR1 | 用户自定义signal 1 |
SIGUSR2 | 用户自定义signal 2 |
SIGVTALRM | setitimer函数设置的Virtual Interval Timer超时的时候 |
SIGWAITING | Solaris Thread Library内部实现专用 |
SIGWINCH | 当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程 |
SIGXCPU | 当CPU时间限制超时的时候 |
SIGXFSZ | 进程超过文件大小限制 |
SIGXRES | Solaris专用,进程超过资源限制的时候发送 |
注意
- 不要使用低级的或者STDIO.H的IO函数
- 不要使用对操作
- 不要进行系统调用
- 不是浮点信号的时候不要用longjmp
- signal函数是由ISO C定义的。因为ISO C不涉及多进程,进程组以及终端I/O等,所以他对信号的定义非常含糊,以至于对UNIX系统而言几乎毫无用处。
- 备注:因为signal的语义与现实有关,所以最好使用sigaction函数替代本函数
当某个信号出现时,系统有三种处理方式:
- 忽略信号:大多数信号使用,但SIGKIL和SIGSTOP不能被忽略
- 捕捉信号:通知内核在某种信号发生时,调用一个用户函数
- 执行系统默认动作:异常终止(abort)、退出(exit)、忽略(ignore)、停 止(stop)或继续(continue)
功能
- 发送信号:发送进程把信号送到指定进程信号域的某一位上,如目标进程正在一个可被中断的优先级上睡眠,核心便将其唤醒
- 预置对信号的处理方式:进程处于核心态时,即使受到软中断也不予理睬;只有当它返回到用户态后,才处理软中断信号
- 收受信号的进程按事先规定完成对相应事件的处理
进程的软中断通信——函数的使用
向一个进程或一组进程发送一个信号:int kill(pid, sig)
pid>0时,核心将信号发送给进程pid
pid<0时,核心将信号发送给与发送进程同组的所有进程
pid=-1时,核心将信号发送给所有用户标识符真正等于发送进程的有 效用户标识号的进程预置信号接收后的处理方式:signal(sig, function)
function=1时,屏蔽该类信号
function=0时,收到sig信号后终止自己
function为非0、非1类整数时,执行用户设置的软中断处理程序
Linux进程间通信—管道和有名管道
- 管道用于具有亲缘关系进程间的通信
- 管道是半双工的,数据只能单向流动(双方通信需建立两个管道)
- 管道只能用于父子进程或兄弟进程之间
- 管道对于管道两端的进程而言就是一个文件,并单独构成一种文件 系统,存在于内存中
- 写管道的内容添加在管道缓冲区的末尾,读管道则从缓冲区头部读 出
- 有名管道在普通管道具备功能基础上,通过给管道命名的 方法变成管道文件,允许无亲缘关系进程间通过访问管道 文件进行通信
无名管道的使用
- int pipefd[2]; int pipe(pipefd); /*创建无名管道*/
pipefd[0]只能用于读; pipe[1]只能用于写
pipe函数定义中的fd参数是一个大小为2的一个数组类型的指针。该函数成功时返回0,并将一对打开的文件描述符值填入fd参数指向的数组。失败时返回 -1并设置errno。
通过pipe函数创建的这两个文件描述符 fd[0] 和 fd[1] 分别构成管道的两端,往 fd[1] 写入的数据可以从 fd[0] 读出。并且 fd[1] 一端只能进行写操作,fd[0] 一端只能进行读操作,不能反过来使用。要实现双向数据传输,可以使用两个管道。
默认情况下,这一对文件描述符都是阻塞的。此时,如果我们用read系统调用来读取一个空的管道,则read将被阻塞,知道管道内有数据可读;如果我们用write系统调用往一个满的管道中写数据,则write也将被阻塞,直到管道有足够的空闲空间可用(read读取数据后管道中将清除读走的数据)。当然,用户可自行将 fd[0] 和 fd[1] 设置为非阻塞的。
如果管道的写端文件描述符 fd[1] 的引用计数减少至0,即没有任何进程需要往管道中写入数据,则对该管道的读端文件描述符 fd[0] 的read操作将返回0(管道内不存在数据的情况),即读到了文件结束标记(EOF,End Of File);反之,如果管道的读端文件描述符 fd[0] 的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符 fd[1] 的write操作将失败,并引发SIGPIPE信号(往读端被关闭的管道或socket连接中写数据)。
管道内部传输的数据是字节流,这和TCP字节流的概念相同。但它们又存在细微的差别。应用层程序能往一个TCP连接中写入多少字节的数据,取决于对方接受窗口的大小和本端的拥塞窗口的大小。而管道的话本身拥有一个容量限制,它规定如果管道的写端应用程序不将管道中数据读走的话,该管道最多还能被写入多少字节的数据。管道容量的大小默认是65536字节。我们也可以使用fcntl函数来修改管道容量。
父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
- 将数据写入管道:write()
- 函数原型:
int write(int handle, void *buf, int nbyte)
- 管道长度受到限制,管道满时写入操作将被阻塞,直到管道中的 数据被读取
- fcntl()可将管道设置为非阻塞模式
- 函数原型:
- 从管道读取数据:read()
- 函数原型:
ssize_t read (int fd, void *buf, size_t count);
- 返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0。
- 参数:参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。
read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。 - 当数据被读取后,数据将自动被管道清除
- 不能由一个进程向多个进程同时传递同一个数据
- fcntl()可将管道读模式设置为非阻塞模式
- 函数原型:
- 关闭管道:close()
- 函数原型:
int close(int fd);
- 返回值:成功返回0,出错返回-1并设置errno
- 参数fd是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
- 关闭读端口时,在管道上进行写操作的进程将收到SIGPIPE信号
- 关闭写端口时,进行读操作的read()函数将返回0
- 函数原型:
管道通信的使用—命名管道的创建与读写
- 创建命名管道:
1
2int mknod(const char *path, mode_t mod, dev_t dev);
int mkfifo(const char *path, mode_t mode); - 命名管道必须先调用open()将其打开
- 同时用读写方式(O_RDWR)打开时,一定不会导致阻塞
- 以只读方式(O_RDONLY)打开时,调用open()函数的进程将会被 阻塞直到有写方打开管道
- 以写方式(O_WRONLY)打开时,阻塞直到有读方打开管道
实例
实现进程简单控制和利用管道通信
1 |
|