Linux - 进程
什么是进程
一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源。
可以把进程看作是正在运行的程序,每个运行着的程序实例就构成一个进程。
进程的结构
内存结构
PID——代码——数据——函数库——文件
PID(进程标识符)
每个进程都会分配一个唯一的数字编号PID,通常取值范围为2-32768,进程启动时,系统将按照顺序选择下一个未被使用的数字作为他的PID,PID自动增长,在超过最大值后,从2开始寻找释放后的PID使用。
数字1,一般是为特殊进程init保留的,init进程负责管理其他进程,
进程管理方式
Linux进程不能用来怼存放代码的内存区进行写操作,程序代码以只读的方式加载到内存中,被多个进程安全的共享,同时共享系统函数库。
但是进程之间数据区和堆栈区都不相同,并且拥有独立的换进空间(专门为某个进程建立的环境变量),保存函数中局部变量和控制函数的调用和返回。
STAT
使用ps ax 命令后
观察STAT
S 睡眠,等待某个时间的发生,信号/输入
R 运行,可运行,在运行队列中,处于正在执行或即将执行态
D 不可中断的睡眠(等待),通常是等待输入或者输出完成
T 停止。通常是被shell作业控制停止,或者进程正处于调试器的控制之下
Z 死(Defunct)或者僵尸进程(zombie)
N 低优先级任务,nice
W 分页,不适合2.6版本linux内核
s 进程属于会话期首进程
* 进程属于前台进程组
l 进程属于多线程的
< 高优先级任务
父进程和子进程
一般来说 每个进程都是由另一个我们称之为父进程的进程启动的,被父进程启动的进程叫做子进程。
Linux系统运行时,他将运行一个名为init的进程,该进程时系统运行的第一个进程,它的进程号为1,你可以把init进程看作为操作系统的进程管理器,他是其他所有进程的祖先进程,我们将要看到的其他的系统进程,要买么就是由init进程启动的,要么就是被init启动的其他进程启动的。
进程调度
最后一行 表示进程4006处于运行状态(R+),正在执行的命令是ps ax,这个进程出现在自己的输出结果中了,这个状态指示符,只表示程序以准备好运行,并不意味着它正在运行,在一台单处理器计算机上,同一时间只能由一个进程可以运行,其他程序处于等待运行状态,每个进程轮到的运行时间(时间片)是相当短暂的,造成了多程序同时运行的假象。
状态R+只表示这个程序是一个前台任务,他不是在等待其他进程结束,或者输入输出操作完成,
Nice程序
在一个如同linux这样的多任务系统中,多个程序可能会竞争使用同一个资源,这种情况下,执行短期突发性工作并暂停运行来等待输入的程序,要比持续占用处理器来进行计算或不断轮询系统来查看是否有新的输入到达的程序要更好。
我们称表现良好的城西为nice程序,而且在某种意义上,nice可以被计算出来,操作系统根据程序的nice值决定他的优先级,一个进程默认的nice值为0并将根据这个程序的表现不断变化。
长时间不间断的程序优先级一般会比较低,暂停等待输入等操作的程序会得到奖励,这可以帮助与用户交互的程序保持及时的相应性,程序在等待用户输入时候,系统会增加它的优先级,这样当它准备继续运行时,他就拥有较高的优先级,优先执行。
我们可以使用nice命令设置进程的nice值,使用renice调整他的nice值。
nice可以使得nice值增加10,从而降低该进程的优先级。使用ps-l(f)选项查看正在运行进程的nice的值。
创建新进程的方式
system函数
在一个程序内部启动另一个程序,从而创建一个新进程,可以通过库函数system来完成。
#include<stdlib.h>
int system(const char *string)
程序入口——》system函数调用-》ps-》system调用返回-》程序继续(结束)
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 //int system(const char *string)
5 //运行以字符串参数形式传递给它的命令
6 //等待 命令的完成
7 //无法启动shell运行这个命令就返回127 其他错误返回-1 否则返回该命令的退出吗
8 int main()
9 {
10 printf("Running ps with system\n");
11 system("ps l");
12 printf("Done.\n");
13 exit(0);
14 }
程序入口-》system函数调用-》system函数调用返回-》程序结束
|
ps-》指令结束
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 //int system(const char *string)
5 //运行以字符串参数形式传递给它的命令
6 //等待 命令的完成
7 //无法启动shell运行这个命令就返回127 其他错误返回-1 否则返回该命令的退出吗
8 int main()
9 {
10 printf("Running ps with system\n");
11 system("ps l &");
12 printf("Done.\n");
13 exit(0);
14 }
程序一 顺序执行
程序二 执行到ps处源程序顺序执行 ps程序在后台独自运行直到结束
exec 系列替换进程映像
//execl execlp execle 参数是可变的 参数以一个空指针结束
//execv execvp 第二个参数十一个字符串数组
//通常以上都是由execve实现的
//p结尾的函数 搜索PATH环境变量来查找新城需的可执行文件的路径
//全局变量environ可以用来把一个值传递到新的程序环境中
//execle和execve可以通过参数envp传递字符串数组作为新程序的环境变
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 int main()
5 {
6 char *const psArgv[] = {"ps","ax",0};
7
8 char *const psEnvp[] = {"PATH = /bin:/usr/bin","TERM=console",0};
9
10 //空指针结尾
11 printf("11111111111111\n");
12 execl("bin/ps","ps","ax",0);
13 printf("2222222222222222\n");
14 execlp("ps","ps","ax",0);
15 printf("333333333333333333333\n");
16 execle("/bin/ps","ps","ax",0,psEnvp);
17 printf("444444444444444\n");
18 execv("/bin/ps",psArgv);
19 printf("555555555555555\n");
20 execvp("ps",psArgv);
21 printf("66666666666666666\n");
22 execve("/bin/ps",psArgv,psEnvp);
23 return 0;
24 }
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 printf("Running ps with execlp\n");
8 execlp("ps","ps","l",0);
9 printf("Done.\n");
10 exit(0);
11 }
可以观察得到 ps正常输出 字符串Done没有输出 且没有任何exec的信息
程序先打印出第一条信息,接着调用execlp 这个函数在PATH环境变量给出的目录中搜索程序ps,然后使用ps替换exec(原程序),ps命令结束后 直接退出,给出一个新的shell提示符,不再回到源程序中。
新进程的PID、PPID、nice值与原先的相同,运行中的程序开始执行exec调用中指定的新的可执行文件中的代码
发生错误后exec函数返回-1,并且设置错误变量errno,由exec启动的新进程继承了原进程的许多特性,在源程序中打开的文件描述符,在新进程中仍然保持打开,除非执行时关闭标志被置位。任何在源程序中已打开的目录流都将在新进程中被关闭。
fork函数 复制进程映像
要想让进程同时执行多个函数,我们可以使用线程或者从源程序中创建一个完全分离的进程,后者的做法等同于init,不像exec调用新程序替换当前执行的线程。
我们可以通过fork创建一个新进程,这个系统复制调用当前进程,在进程表中创建一个新的表项,新表项的属性和当前进程基本相同,新进程几乎和原进程一致,执行的代码也完全相同,但新进程有了自己的数据空间,环境和文件描述符。
在父进程中fork调用返回的是新的子进程的PID,新进程将继续执行,就像原进程一般,子进程中fork返回的是0,可以通过这个判断父子进程。fork失败返回-1,失败通常是因为父进程所拥有的子进程数目超过了规定(CHILD_MAX)此时errno将被设为EAGAIN,如果是因为进程表中没有足够的空间用于创建新的表单或虚拟内存不足,errno 将被设为ENOMEM
#include<sys/types.h>
#include<unistd.h>
pid fork(void)
1 #include<sys/types.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<stdio.h>
5
6 int main()
7 {
8 char *message;
9 int n;
10 printf("fork program starting\n");
11 pid_t newPid = fork();
12
13
14 switch(newPid)
15 {
16 case -1:
17 perror("fork failed");//上一个函数发生错误的原因输出到标准设备,
18 exit(0); //参数会先打印后面加上错误原因字符串
19 case 0:
20 message = "This is the child ";
21 n = 5;
22 break;
23 default:
24 message = "This is the parent";
25 n = 3;
26 break;
27 }
28
29 for(;n>0;n--)
30 {
31 puts(message);
32 sleep(1);
33 }
34
35 exit(0);
36 }
这个程序以两个进程的形式在运行,父进程输出比子进程少,在子进程打印完之前就打印结束了,shell提示符出现,子进程尚未打印完毕,子进程继续打印,直到进程结束。
进程操作
wait等待一个进程
当用fork启动一个子进程后,子进程就有了自己的生命周期并将独立运行,如何知道子进程何时结束。在父函数中调用wait函数让父进程等待子进程的结束。
wait系统调用将暂停父进程直到它的子进程结束为止,这个调用返回子进程的PID,它通常是已经结束运行的子进程的PID,状态信息允许父进程了解子进程的退出状态,即子进程的main函数返回的值或子进程中exit函数的退出码。
如果stat_loc不是空指针,状态信息将被写入它指向的位置,我们可以使用sys/wait.h文件中定义的宏来解释状态信息
WIFEXITED(stat_val) 如果子进程正常结束,他就取一个非0值
WEXITSTATUS(stat_val) 如果WFIFEXITED非0,它返回子进程的退出码。
WIFSIGNALED(stat_val) 如果子进程因为一个未捕获的信号而终止,他就取一个非0值
WTERMSIG(stat_val) 如果 WIFSIGNALED非0,他返回一个信号代码
WIFSTOPPED(stat_val) 如果子进程意外终止,返回一个非0值
WSTOPSIG(stat_val) 如果WIFSTOPPED非0 它返回一个信号代码
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *stat_loc)
1 #include<sys/types.h>
2 #include<sys/wait.h>
3 #include<unistd.h>
4 #include<stdio.h>
5 #include<stdlib.h>
6
7
8 int main()
9 {
10 pid_t pid;
11 char *message;
12 int n;
13 int exitCode;
14
15 printf("fork program starting\n");
16 pid = fork(); //父进程中返回子进程ID 子进程中返回0 错误返回负数
17 switch(pid)
18 {
19 case -1:
20 perror("fork failed");
21 exit(1);
22 case 0:
23 message = "This is a child";
24 n = 5;
25 exitCode = 37;
26 break;
27 default:
28 message = "This is the parent";
29 n = 3;
30 exitCode = 0;
31 break;
32 }
33
34 for(;n>0;n--)
35 {
36 puts(message);
37 sleep(1);
38 }
39
40 if(pid != 0) //父进程返回子进程PID 子进程为0
41 {
42 int statVal; //存储子进程的结束状态
43 pid_t childPid;
44
45 childPid = wait(&statVal); //
46
47 printf("Child has finished :PID = %d\n",childPid);
48 if(WIFEXITED(statVal)) //如果子进程正常结束,他就返回一个非0值
49 printf("Child exited with code %d\n",WEXITSTATUS(statVal)); //如//果WIFEXITED非0,返回子进程退出码
50 else
51 printf("Child terminated abnormally\n");
52 }
53 exit(exitCode);
54 }
父进程(从fork调用中获得一个非0的返回值),用wait系统调用将自己的执行挂起,直到子进程的状态信息出现为止,这将发生在自己成调用exit的时候,我们将子进程的退出码设置为37.
父进程然后继续运行,通过测试wait调用的返回值来判断子进程是否正常终止,如果是就从状态信息中取出子进程的退出码。
僵尸进程
用fork创建进程必须了解子进程运行情况,子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才告结束,因此进程表中代表子进程的表项不会立刻释放,虽然子进程已经不再运行,但它任然存在于系统中,因为它的退出码还要保存起来,以被父进程今后的wait调用,这时他会成为一个死/僵尸进程。
1 #include<sys/types.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<stdio.h>
5
6 int main()
7 {
8 char *message;
9 int n;
10 printf("fork program starting\n");
11 pid_t newPid = fork();
12
13
14 switch(newPid)
15 {
16 case -1:
17 perror("fork failed");//上一个函数发生错误的原因输出到标准设备,
18 exit(0); //参数会先打印后面加上错误原因字符串
19 case 0:
20 message = "This is the child ";
21 n = 3;
22 break;
23 default:
24 message = "This is the parent";
25 n = 5;
26 break;
27 }
28
29 for(;n>0;n--)
30 {
31 puts(message);
32 sleep(1);
33 }
34
35 exit(0);
36 }
在子进程结束之后,父进程尚未结束的时候调用ps -al 查看进程表 PID为3785的进程成为了僵尸进程,如果此时父进程异常终止,子进程自动把PID为1的进程(init)作为自己的父进程,子进程是一个不再运行的僵尸进程,父进程异常终则由init接管,僵尸进程将一直保留在进程表中,直到被init进程发现并释放,进程表越大,这一过程就越慢,应该尽量避免产生僵尸进程,因为在init清理他们之前他们将一直消耗电脑资源。
waitpid等待一个特定进程
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *stat_loc, int options)
pid 参数需要指定需要等待的子进程的PID。如果它的值为-1,waitpid 将返回任一子进程的信息。与wait一样,如果stat_loc 不是空指针,waitpid 将把状态信息写到它所指的位置,option参数可以改变waitpid 的行为,比如WNOGANG可以防止waitpid调用将调用者的执行挂机。查找是否有子进程已经结束,没有将继续执行。
让父进程周期性检查某个特定的子程序是否终止
waitpid(child_pid , (int*)0 , WNOHANG);
如果子进程没有结束或者意外终止,他就返回0,否则返回child_pid 如果waitpid失败,他将返回-1并设置errno,失败的情况包括:没有子进程(errno设置为ECHILD)、调用某个信号中断(EINTR)或选项参数无效(EINVAL)。
输入输出重定向
已打开的文件描述符将在fork和exec调用之后保留下来,我们可以利用进程来改变程序的行为。
1 #include<stdio.h>
2 #include<ctype.h>
3 #include<stdlib.h>
4
5 int main()
6 {
7 int ch;
8 while((ch =getchar()) != EOF)
9 {
10 putchar(toupper(ch)); //toupper转化小写字母为大写字母
11 }
12 exit(0);
13 }
利用shell重定向把一个文件的内容全部转化为大写
在另一个程序中使用这个过滤程序,接受一个文件名作为命令行参数,如果对他的调用不正确,他将响应一个错误信息。
1 #include<unistd.h>
2 #include<stdio.h>
3 #include<stdlib.h>
4
5 int main(int argc,char *argv[])
6 {
7 char *filename;
8
9 if(argc != 2)
10 {
11 fprintf(stderr,"usage:useupper file\n");
12 //stdout (标准输出) 行缓冲,输出字符放在缓冲区 回车后I/O操作
13 //stderr(标准错误)不带缓冲 直接输出 尽快显示
14 //fprintf(FILE *stream, const char *format,...);
15 exit(1);
16 }
17
18 filename = argv[1];
19
20 if(!freopen(filename,"r",stdin))
21 //freopen(const char *filename,const char *mode,FILE *stream)
22 //要打开的文件名 文件打开的模式 文件指针
23 {
24 fprintf(stderr,"couldddddddd not redirect stdin from file %s\n",filename);
25 exit(2);
26 }
27
28 execl("./upper","upper",0);
29 perror("could not exec./upper\n");
30 //打印上一个函数的错误信息 格式 字符+错误信息
31 exit(3);
32 }
upper2程序用freopen函数先关闭标准输入,然后将文件流stdin与程序参数给定的文件名关联起来,接下来调用execl用upper程序 替换掉正在运行的进程代码,因为已打开的文件描述符和execl调用之后保留下来,所以成功执行。
信号
简介
信号是Linux/UNIX系统响应某些条件而产生的一个事件,接收到该信号的进程会相应的采取一些行动。信号的产生都是生成(raise),信号的接收都使用捕获(catch)
.
信号是由于某些错误条件生成的,内存段冲突,浮点处理器错误或者是非法指令,它们由shell和终端处理器生成来引起中断,他们还可以作为在进程间传递消息或者修改行为的一种方式,明确的由一个进程发送给另一个进程,无论何种情况,他们的程序接口是相同的,信号可以被生成、捕获、响应、(对于一些信号)忽略。
SIGABORT *程序异常终止
SIGALRM 超时警告
SIGFPE *浮点运算异常
SIGHUP 连接挂断
SIGILL *非法指令
SIGINT 终端中断(Ctrl +C 向前端进程发送和)
SIGKILL 终止进程(此信号不能被捕获或忽略)
SIGPIPE 向无读进程的管道写数据
SIGQUIT 终端推出
SIGSEGV *无效内存访问
SIGTERM 终止
SIGUSR1 用户定义信号1
SIGUSR2 用户定义信号2
SIGCHLD 子进程停止或退出
SIGCONT 继续执行暂停进程
SIGSTOP 停止执行
SIGTSTP 终端挂起
SIGTTIN 后台进程尝试读
SIGTTOU 后台进程尝试写
*系统对信号的响应视具体情况而定
如果进程接收到这些信号中的一个,事先没有安排捕获他,进程将会立刻中止,通常系统将生成核心转储文件core,并将其放在当前目录下,该文件是进程在内存中的映像.
kill 发送信号给进程 kill -HUP 512
killall 给所有运行某一命令进程发信号 killall -HUP inetd
#include<signal.h>
void(*signal (int sig, void (*func)(int)))(int)
signal有两个参数
sig给出准备捕获或忽略得信号
func保存接收到信号后调用得函数
信号处理类函数必须有一个int参数(接受的信号代码)且返回值为void
代替信号处理函数
SIG_LGN 忽略信号
SIG——DFL 恢复默认行为
1 #include<signal.h>
2 #include<stdio.h>
3 #include<unistd.h>
4
5 void ouch(int sig)
6 {
7 printf("OUCH - I got signal %d\n",sig);
8 (void)signal(SIGINT,SIG_DFL);
9 }
10
11 int main()
12 {
13 (void) signal(SIGINT,ouch);
14
15 while(1)
16 {
17
18 printf("Hello World\n");
19 sleep(1);
20 }
21 }
signal 函数返回一个先前对指定信号进行处理得信号处理函数得函数指针。未定义信号处理函数,返回SIG_ERR设置errno为一个正数值,如果给出的是一个无效得信号或者是尝试处理得信号是不可捕获或不可忽略得信号errno将被设置为EINVAL。
发送信号
进程可以通过调用kill函数向包括它本身在内得其他进程发送一个信号,如果程序没有发送权限,调用kill就会失败
#include<sys/types.h>
#include<signal.h>
int kill (pid_t,int sig)
将sig给出得信号传递给pid进程号指定得进程
成功返回0 失败返回-1设置errno
给定信号无效 EINVAl 发送进程权限不够 EPERM 目标进程不存在 ESRCH
权限:两个进程要有相同得用户ID,只能给自己得进程发送信号
信号还有一个闹钟功能,进程可以通过alarm函数在经过预定的时间后发送一个信号
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
在参数秒后发送信号 除了延时和时间调度的不确定性,实际闹钟略晚,参数设置为0 取消所有闹钟,收到信号前调用alarm函数,闹钟重新计时,一个进程只能有一个闹钟,返回值是预留秒数,失败返回1
模拟闹钟
1 #include<sys/types.h>
2 #include<signal.h>
3 #include<stdio.h>
4 #include<unistd.h>
5 #include<stdlib.h>
6
7
8 static int alarmFired = 0;
9
10 void ding(int sig)
11 {
12 alarmFired = 1;
13 }
14
15 int main()
16 {
17 pid_t pid;
18 printf("alarm application starting \n");
19
20 pid = fork();
21 switch(pid)
22 {
23 case -1:
24 perror("fork failed~");
25 exit(1);
26 case 0:
27 //child
28 sleep(5);
29 kill(getppid(),SIGALRM);
30 exit(0);
31 }
32
33 printf("waiting for alarm to go off\n");
34 (void) signal(SIGALRM,ding);
35
36 pause();
37 //进程挂起 直到等到一个信号为止
38 if(alarmFired)
39 printf("Ding !\n");
40
41 printf("Done~ \n");
42 exit(0);
43 }
#include<signal.h>
int sigaction(int sig,const struct sigaction *act ,struct sigaction *oact);
sigaction结构体在头文件中,它定义了接收到参数sig指定的信号后因该该区的行动,oact不空,原先动作写入,为空不设置。
act指向的sigaction结构中 sa_handler是一个函数指针,它指向接收信号sig时被调用的信号处理函数,sa_mask指定了一个信号集,调用sa_handler所指向处理函数之前,信号集加入到进程的信号屏蔽字中,这是一组将被阻塞且不会传递给该进程的信号,可以防止前面看到的信号在他的处理函数未结束时就被接收到。可以消竞态条件,信号处理函数默认状态不被重置。sa_flags成员中包含值SA_RESETHAND
1 #include<signal.h>
2 #include<stdio.h>
3 #include<unistd.h>
4
5 void ouch(int sig)
6 {
7 printf("OUCH - I got signal %d\n",sig);
8 }
9
10 int main()
11 {
12 struct sigaction act;
13 act.sa_handler = ouch;
14 sigemptyset(&act.sa_mask);
15 act.sa_flags = 0;
16
17 sigaction(SIGINT,&act,0);
18
19 while(1)
20 {
21 printf("6566666666~~~~~~~~\n");
22 sleep(1);
23 }
24 }
// ctrl +c SIGINT
//ctrl +\ SIGOUT
信号集
头文件signal.h定义了类型sigset_t和用来处理信号集的函数,可以通过修改信号集修改进程在接收到信号时的行为。
#include<signal.h>
int sigaddset(sigset_t *set, int signo); //增加信号
int sigemptyset(sigset_t *set); //将信号集初始化为空
int sigfillset(sigset_t *set); //初始化为包含所有已定义信号
int sigdelset(sigset_t *set, int signo); //删除给定信号
#include<signal.h>
判断一个给定的信号是否是一个信号集成员
int sigismember(sigset_t *set, int signo);
进程信号屏蔽字设置检查由sigprocmask完成(被屏蔽的不能被当前进程接收到)
int sigprocmask(int how, const sigset_t *set,sigset_t *oset);
how
SIG_BLOCK 把参数set中的信号加入信号屏蔽字
SIG_SETMASK 把信号屏蔽字设置为参数set'中的信号
SIG_UNBLOCK 从信号屏蔽字中删除参数set中的信号