兰州大学操作系统实验五进程管理题目和答案
说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。
实验五 实验五 实验名称: 进程管理 实验目的: 1. 进一步学习进程的属性 2. 学习进程管理的系统调用 3. 掌握使用系统调用获取进程的属性、创建进程、实现进程控制等 4. 掌握进程管理的基本原理 实验时间 pcb 6学时 预备知识: 1. 进程属性 1.1 getpid(取得进程ID) 表头文件 #include 定义函数 pid_t getpid(void); 函数说明 getpid()用来取得目前进程的进程ID,许多程序利用取到的此值来建立临时文件,以避免临时文件相同带来的问题。 返回值 目前进程的进程ID 范例 1.2 getppid(取得父进程的进程ID) 表头文件 #include 定义函数 pid_t getppid(void); 函数说明 getppid()用来取得目前进程的父进程ID。 返回值 目前进程的父进程ID。 1.3 getegid(取得有效的组ID) /etc/shadow 存放用户口令信息,主人是root 表头文件 #include 默认是只读,当然可以修改自己的这个权限 #include 其他用户可以通过申请,修改自己的这些 定义函数 gid_t getegid(void); ls -l /usr/bin/passwd 函数说明 getegid()用来取得执行目前进程有效(effective)组ID。有效的组ID用来决定进程执行时组的权限。返回值返回有效的组ID。 Group 组 1.4 geteuid(取得有效的用户ID) 表头文件 #include #include 定义函数 uid_t geteuid(void) 函数说明 geteuid()用来取得执行目前进程有效的用户ID。有效的用户ID用来决定进程执行的权限,借由此改变此值,进程可以获得额外的权限。倘若执行文件的setID位已被设置,该文件执行时,其进程的euid值便会设成该文件所有者的uid。 返回值 返回有效的用户ID。 1.5 getgid(取得真实的组ID) 表头文件 #include #include 定义函数 gid_t getgid(void); 函数说明 getgid()用来取得执行目前进程的组ID。 返回值 返回组ID 1.6 getuid(取得真实的用户ID) 表头文件 #include #include 定义函数 uid_t getuid(void); 函数说明 getuid()用来取得执行目前进程的用户ID。 返回值 用户ID 1.7 times (取得进程相关的时间) 断异常 表头文件 #include 定义函数 clock_t times(struct tms *buf); 函数说明 取得进程运行相关的时间。 参数说明 /*sys/times.h*/ struct tms{ } clock_t tms_utime; clock_t tms_stime; clock_t tms_cutime; clock_t tms_cstime; /*进程花在执行用户模式代码上的时间*/ <=几十毫秒 /*进程花在执行内核代码上的时间*/ <=几十毫秒 /*子进程花在执行用户模式代码上的时间*/ /*子进程花在执行内核代码上的时间*/ 返回值 自系统自举后经过的时钟嘀嗒数。 注意 时钟嘀嗒数time转换为用户可读的方式,即多少秒,需通过如下方式: (float)time/sysconf(_SC_CLK_TCK); sysconf:宏 代表一常量 2. 进程创建 2.1 system(执行shell 命令) 通过执行命令产生一个进程 表头文件 #include 定义函数 int system(const char * string); man sh -c(不覆盖现有的文件“>”。) 函数说明 system()会调用fork()产生子进程,由子进程来调用/bin/sh -c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。 返回值 如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。若参数string为空指针(NULL),则返回非零值。 2.2 fork(建立一个新的进程) 创建一个新的与父进程一模一样的子进程 表头文件 #include 定义函数 pid_t fork(void); 函数说明 fork()会产生一个新的子进程,其子进程会复制父进程的数据与堆栈空间,并继承父进程的用户代码,组代码,环境变量、已打开的文件代码、工作目录和资源限制等。Linux 使用copy-on-write(COW)技术,只有当其中一进程试图修改欲复制的空间时才会做真正的复制动作,由于这些继承的信息是复制而来,并非指相同的内存空间,因此子进程对这些变量的修改和父进程并不会同步。此外,子进程不会继承父进程的文件锁定和未处理的信号。 注意 Linux不保证子进程会比父进程先执行或晚执行,因此编写程序时要留意死锁或竞争条件的发生。 返回值 如果fork()成功则在父进程会返回新建立的子进程代码(PID),而在新建立的子进程中则返回0。如果fork 失败则直接返回-1,失败原因存于errno中。 错误代码 EAGAIN 内存不足。ENOMEM 内存不足,无法配置核心所需的数据结构空间。 exec函数簇 表头文件 #include 2.3.1 execl(执行文件) 定义函数 int execl(const char * path,const char * arg,....); 函数说明 execl()用来执行参数path字符串所代表的文件路径,接下来的参数代表执行该文件时传递过去的argv(0)、argv[1]„„,最后一个参数必须用空指针(NULL)作结束。 返回值 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。 范例 #include main() { printf(“BEFORE”); execl(“/bin/ls”,”ls”,”-al”,”/etc/passwd”,(char * )0); printf(“AFTER”); //这里的东西被前面覆盖了,被回收了。 } 2.3.2 execlp(从PATH 环境变量中查找文件并执行) 定义函数 int execlp(const char * file,const char * arg,„„); 函数说明 execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]„„,最后一个参数必须用空指针(NULL)作结束。 返回值 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno 中。 错误代码 参考execve()。 范例 /* 执行ls -al /etc/passwd execlp()会依PATH 变量中的/bin找到/bin/ls */ #include main() { execlp(“ls”,”ls”,”-al”,”/etc/passwd”,(char *)0); } 2.3.3 execv(执行文件) 定义函数 int execv (const char * path, char * const argv[ ]); 函数说明 execv()用来执行参数path字符串所代表的文件路径,与execl()不同的地方在于execve()只需两个参数,第二个参数利用数组指针来传递给执行文件。 返回值 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno 中。 范例 /* 执行/bin/ls -al /etc/passwd */ #include main() { char * argv[ ]={“ls”,”-al”,”/etc/passwd”,(char*) 0}; execv(“/bin/ls”,argv); } 2.3.4 execve(执行文件) 定义函数 int execve(const char * filename,char * const argv[ ],char * const envp[ ]); 函数说明 execve()用来执行参数filename字符串所代表的文件路径,第二个参数系利用数组指针来传递给执行文件,最后一个参数则为传递给执行文件的新环境变量数组。 返回值 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno 中。 错误代码 EACCES 1. 欲执行的文件不具有用户可执行的权限。 2. 欲执行的文件所属的文件系统是以noexec 方式挂上。 3.欲执行的文件或script翻译器非一般文件。 EPERM 1.进程处于被追踪模式,执行者并不具有root权限,欲执行的文件具有SUID 或SGID 位。 2.欲执行的文件所属的文件系统是以nosuid方式挂上,欲执行的文件具有SUID 或SGID 位元,但执行者并不具有root权限。 E2BIG 参数数组过大 ENOEXEC 无法判断欲执行文件的执行文件格式,有可能是格式错误或无法在此平台执行。 EFAULT 参数filename所指的字符串地址超出可存取空间范围。 ENAMETOOLONG 参数filename所指的字符串太长。 ENOENT 参数filename字符串所指定的文件不存在。 ENOMEM 核心内存不足 ENOTDIR 参数filename字符串所包含的目录路径并非有效目录 EACCES 参数filename字符串所包含的目录路径无法存取,权限不足 ELOOP 过多的符号连接 ETXTBUSY 欲执行的文件已被其他进程打开而且正把数据写入该文件中 EIO I/O 存取错误 ENFILE 已达到系统所允许的打开文件总数。 EMFILE 已达到系统所允许单一进程所能打开的文件总数。 EINVAL 欲执行文件的ELF执行格式不只一个PT_INTERP节区 EISDIR ELF翻译器为一目录 ELIBBAD ELF翻译器有问题。 范例 #include main() { char * argv[ ]={“ls”,”-al”,”/etc/passwd”,(char *)0}; char * envp[ ]={“PATH=/bin”,0} execve(“/bin/ls”,argv,envp); } 2.3.5 execvp(执行文件) 定义函数 int execvp(const char *file ,char * const argv []); 函数说明 execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。 返回值 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。 范例 /*请与execlp()范例对照*/ #include main() { char * argv[ ] ={ “ls”,”-al”,”/etc/passwd”,0}; execvp(“ls”,argv); } 3. 进程等待 find 查找文件 group 查找字符串 3.1 wait(等待子进程中断或结束) 表头文件 #include #include 定义函数 pid_t wait (int * status); 函数说明 wait()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status 返回,而子进程的进程ID也会一快返回。如果不在意结束状态值,则参数 status可以设成NULL。子进程的结束状态值请参考waitpid()。 返回值 如果执行成功则返回子进程ID(PID),如果有错误发生则返回-1。失败原因存于errno中。 附加说明 范例 #include #include #include #include main() { pid_t pid; int status,i; if(fork()= =0){//子进程代码 printf(“This is the child process .pid =%d\n”,getpid()); exit(5); }else{ //父进程代码 sleep(1); printf(“This is the parent process ,wait for child...\n”; pid=wait(&status); //等待 i=WEXITSTATUS(status);// 唤醒 遗言放在status中 printf(“child’s pid =%d .exit status=^d\n”,pid,i); } } 3.2 waitpid(等待子进程中断或结束) 表头文件 #include #include 定义函数 pid_t waitpid(pid_t pid,int * status,int options); 函数说明 waitpid()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程ID也会一快返回。如果不在意结束状态值,则参数status可以设成NULL。参数pid为欲等待的子进程ID,其他数值意义如下: pid<-1 等待进程组ID为pid绝对值的任何子进程。 pid=-1 等待任何子进程,相当于wait()。 pid=0 等待进程组ID与目前进程相同的任何子进程。 pid>0 等待任何子进程ID为pid的子进程。 参数option可以为0 或下面的OR 组合: WNOHANG 如果没有任何已经结束的子进程则马上返回,不予以等待。 WUNTRACED 如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。 子进程的结束状态返回后存于status,底下有几个宏可判别结束情况 WIFEXITED(status)如果子进程正常结束则为非0值。 WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。 WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真 WTERMSIG(status)取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED 来判断后才使用此宏。 WIFSTOPPED(status)如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。 WSTOPSIG(status)取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED 来判断后才使用此宏。 返回值 如果执行成功则返回子进程ID(PID),如果有错误发生则返回-1。失败原因存于errno中。 4. 信号 4.1 kill(传送信号给指定的进程) 表头文件 #include #include 定义函数 int kill(pid_t pid,int sig); 函数说明 kill()可以用来送参数sig指定的信号给参数pid指定的进程。参数pid有几种情况: pid>0 将信号传给进程ID为pid 的进程。 pid=0 将信号传给和目前进程相同进程组的所有进程 pid=-1 将信号广播传送给系统内所有的进程 pid<0 将信号传给进程组ID为pid绝对值的所有进程 返回值 执行成功则返回0,如果有错误则返回-1。 错误代码 EINVAL 参数sig 不合法 ESRCH 参数pid 所指定的进程或进程组不存在 EPERM 权限不够无法传送信号给指定进程 范例 #include #include #include #include main() { pid_t pid; int status; if(!(pid= fork())){ printf(“Hi I am child process!\n”); sleep(10); return; } else{ printf(“send signal to child process (%d) \n”,pid); sleep(1); kill(pid ,SIGABRT); wait(&status); if(WIFSIGNALED(status)) printf(“chile process receive signal %d\n”,WTERMSIG(status)); } } 4.2 signal(设置信号处理方式) 表头文件 #include 定义函数 void (*signal(int signum,void(* handler)(int))); 函数说明 signal()会依参数signum 指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数handler指定的函数执行。如果参数handler不是函数指针,则必须是下列两个常数之一: SIG_IGN 忽略参数signum指定的信号。 SIG_DFL 将参数signum 指定的信号重设为核心预设的信号处理方式。 返回值 返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。 附加说明 在信号发生跳转到自定的handler处理函数执行后,系统会自动将此处理函数换回原来系统预设的处理方式,如果要改变此操作请改用sigaction()。 5. 进程调度 5.1 getpriority(取得程序进程执行优先权) 表头文件 #include #include 定义函数 int getpriority(int which,int who); 函数说明 getpriority()可用来取得进程、进程组和用户的进程执行优先权。 参数 which有三种数值,参数who 则依which值有不同定义 which who 代表的意义 PRIO_PROCESS who 为进程ID PRIO_PGRP PRIO_USER who 为进程的组ID who 为用户ID 此函数返回的数值介于-20 至20之间,代表进程执行优先权,数值越低代表有较高的优先次序,执行会较频繁。 返回值 返回进程执行优先权,如有错误发生返回值则为-1 且错误原因存于errno。 附加说明 由于返回值有可能是-1,因此要同时检查errno是否存有错误原因。最好在调用次函数前先清除errno变量。 错误代码 ESRCH 参数which或who 可能有错,而找不到符合的进程。EINVAL 参数which 值错误。 5.2 setpriority(设置程序进程执行优先权) 表头文件 #include #include 定义函数 int setpriority(int which,int who, int prio); 函数说明 setpriority()可用来设置进程、进程组和用户的进程执行优先权。参数which有三种数值,参数who 则依which值有不同定义 参数prio介于-20 至20 之间。代表进程执行优先权,数值越低代表有较高的优先次序,执行会较频繁。此优先权默认是0,而只有超级用户(root)允许降低此值。 返回值 执行成功则返回0,如果有错误发生返回值则为-1,错误原因存于errno。 ESRCH 参数which或who 可能有错,而找不到符合的进程 EINVAL 参数which值错误。 EPERM 权限不够,无法完成设置 EACCES 一般用户无法降低优先权 5.3 nice(改变进程优先顺序) 表头文件 #include 定义函数 int nice(int inc); 函数说明 nice()用来改变进程的进程执行优先顺序。参数inc数值越大则优先顺序排在越后面,即表示进程执行会越慢。只有超级用户才能使用负的inc 值,代表优先顺序排在前面,进程执行会较快。 返回值 如果执行成功则返回0,否则返回-1,失败原因存于errno中。 错误代码 EPERM 一般用户企图转用负的参数inc值改变进程优先顺序。 6. 其他 6.1 atexit(设置程序正常结束前调用的函数) 表头文件 #include 定义函数 int atexit (void (*function)(void)); 函数说明 atexit()用来设置一个程序正常结束前调用的函数。当程序通过调用exit()或从main中返回时,参数function所指定的函数会先被调用,然后才真正由exit()结束程序。 返回值 如果执行成功则返回0,否则返回-1,失败原因存于errno中。 范例 #include void my_exit(void) { printf(“before exit () !\n”); } main() { atexit (my_exit); exit(0); } 6.2 exit(正常结束进程) 表头文件 #include 定义函数 void exit(int status); 函数说明 exit()用来正常终结目前进程的执行,并把参数status返回给父进程,而进程所有的缓冲区数据会自动写回并关闭未关闭的文件。 实验要求: 1. 编写一个程序,打印进程的如下信息:进程标识符,父进程标识符,真实用户ID,有效用户ID,真实用户组ID,有效用户组ID。并分析真实用户ID和有效用户ID的区别。 2. 阅读如下程序: /* process using time */ #include #include #include #include #include void time_print(char *,clock_t); int main(void) { } void time_print(char *str, clock_t time) { } 编译并运行,分析进程执行过程的时间消耗(总共消耗的时间和CPU消耗的时间),并解释执行结果。再编写一个计算密集型的程序替代grep,比较两次时间的花销。注释程long tps = sysconf(_SC_CLK_TCK); printf(“%s: %6.2f secs\n”,str,(float)time/tps); exit(EXIT_SUCCESS); puts(“child times”); time_print(“\tuser CPU”,t_end.tms_cutime); time_print(“\tsys CPU”,t_end.tms_cstime); clock_t start,end; struct tms t_start,t_end; start = times(&t_start); system(“grep the /usr/doc/*/* > /dev/null 2> /dev/null”); // > 将信息放到该文件null中 end=times(&t_end); // 0 1 2 标准输入 标准输出 错误输出 time_print(“elapsed”,end-start); puts(“parent times”); time_print(“\tuser CPU”,t_end.tms_utime); time_print(“\tsys CPU”,t_end.tms_stime); 序主要语句。 数字运算(矩阵相乘) 3. 阅读下列程序: /* fork usage */ #include #include #include int main(void) { } 编译并多次运行,观察执行输出次序,说明次序相同(或不同)的原因;观察进程ID,分析进程ID的分配规律。总结fork()的使用方法。注释程序主要语句。 4. 阅读下列程序: /* usage of kill,signal,wait */ #include pid_t child; if((child=fork())==-1{ perror(“fork”); exit(EXIT_FAILURE); }else if(child==0){ puts(“in child”); printf(“\tchild pid = %d\n”,getpid()); printf(“\tchild ppid = %d\n”,getppid()); exit(EXIT_SUCCESS); }else{ } exit(EXIT_SUCCESS); puts(“in parent”); printf(“\tparent pid = %d\n”,getpid()); printf(“\tparent ppid = %d\n”,getppid()); #include #include #include int flag; 子进程 父进程里各有一个flag 没有作用 void stop(); int main(void) { int pid1,pid2; signal(3,stop); while((pid1=fork()) ==-1); if(pid1>0){ while((pid2=fork()) ==-1); if(pid2>0){ flag=1; sleep(5); kill(pid1,16); kill(pid2,17); wait(0); wait(0); printf(“\n parent is killed\n”); exit(EXIT_SUCCESS); }else{ } flag=1; signal(17,stop); printf(“\n child2 is killed by parent\n”); exit(EXIT_SUCCESS); }else{ flag=1; } } signal(16,stop); printf(“\n child1 is killed by parent\n”); exit(EXIT_SUCCESS); void stop(){ } 编译并运行,等待或者按^C,分别观察执行结果并分析,注释程序主要语句。 flag有什么作用?通过实验说明。 5. 编写程序,要求父进程创建一个子进程,使父进程和子进程各自在屏幕上输出一些信息,但父进程的信息总在子进程的信息之后出现。(分别通过一个程序和两个程序实现) 6. 编写程序,要求父进程创建一个子进程,子进程执行shell命令find / -name hda* 的功能,子进程结束时由父进程打印子进程结束的信息。执行中父进程改变子进程的优先级。 7. 编写程序,要求父进程创建一个子进程,子进程对一个50*50的字符数组赋值,由父进程改变子进程的优先级,观察不同优先级进程使用CPU的时间。 8. 编写一个程序,模拟实现一个简单的进程管理子系统,它由进程建立模块、进程撤销模块、进程控制表组成。该子系统通过循环显示“please input new command”接收新进程,根据用户键入内容(命令)启动新进程,然后不等待新进程结束就显示“please input new command” 接收新进程。建立和撤销进程时修改进程控制表。 9. 查阅Linux系统中struct task_struct (有很多结构体类型的指针,至少写出三级指针)的定义,说明每项成员的作用。(熟记该嵌套结构体) Struct Task_struct{ *-------------------------------------- 新的结构体定义{ *--------------------------------------- 新的结构体定义 } } flag = 0; 实验报告: 实验名称: 进程管理 实验报告: 实验要求: cat /etc/group (查看组信息) 10. 编写一个程序,打印进程的如下信息:进程标识符,父进程标识符,真实用户ID,有效用户ID,真实用户组ID,有效用户组ID。并分析真实用户ID和有效用户ID的区别。 代码如下: #include #include int main(){ printf("***********\n"); printf("This is the process\n"); printf(" pid=%d\n",getpid()); printf("ppid=%d\n",getppid()); printf(" uid=%d\n",getuid()); printf("euid=%d\n",geteuid()); printf(" gid=%d\n",getgid()); printf("egid=%d\n",getegid()); } 真实用户ID和有效用户ID的区别: 真实用户ID:这个ID就是我们登陆unix系统时的身份ID。 有效用户ID:定义了操作者的权限。有效用户ID是进程的属性,决定了该进程对文件的访问权限. 11. 阅读如下程序: /* process using time */ #include #include #include #include #include void time_print(char *,clock_t); int main(void) { clock_t start,end; struct tms t_start,t_end; start = times(&t_start); system(“grep the /usr/doc/*/* > /dev/null 2> /dev/null”); // > 将信息放到该文件null中 end=times(&t_end); // 0 1 2 标准输入 标准输出 错误输出 time_ print(“elapsed”,end-start); puts(“parent times”); time _print(“\tuser CPU”,t_end.tms _utime); time_ print(“\tsys CPU”,t_end.tms_stime); //获得执行system()的子进程ID } void time_print(char *str, clock_t time) { long tps = sysconf(_SC_CLK_TCK);/*函数sysconf()的作用为将时钟滴答数转化为exit(EXIT_SUCCESS); puts(“child times”); time_print(“\tuser CPU”,t_end.tms_cutime); time_print(“\tsys CPU”,t_end.tms_cstime); 秒数,_SC_CLK_TCK 为定义每秒钟有多少个滴答的宏*/ } 编译并运行,分析进程执行过程的时间消耗(总共消耗的时间和CPU消耗的时间),并解释执行结果。再编写一个计算密集型的程序替代grep,比较两次时间的花销。注释程序主要语句。 因为该程序计算量很小,故消耗的时间比较少,均为0.00secs 不奇怪。 而更改为计算密集型的之后就较容易观察出消耗时间的差异,如图所示。 printf(“%s: %6.2f secs\n”,str,(float)time/tps); 12. 阅读下列程序: /* fork usage */ #include #include #include int main(void) { } 编译并多次运行,观察执行输出次序,说明次序相同(或不同)的原因;观察进程ID,分析进程ID的分配规律。总结fork()的使用方法。注释程序主要语句。 创建进程ID开始时一般随机分配,但若多次运行,或创建子进程时,会顺序分配内存。此外,当父进程结束时,子进程尚未结束,则子进程的父进程ID变为1,即init fork()的使用方法: fork()会产生一个新的子进程,其子进程会复制父进程的数据与堆栈空间,如果fork()成功则在父进程会返回新建立的子进程代码(PID),而在新建立的子进程中则返回0。如果fork 失败则直接返回-1,失败原因存于errno中。 在父进程中用fork()创建子进程,通过返回值 if语句判断来进行父子进程代码执行。 pid_t child; if((child=fork())==-1{ perror(“fork”); exit(EXIT_FAILURE); }else if(child==0){ puts(“in child”); printf(“\tchild pid = %d\n”,getpid()); //取得目前进程的进程ID printf(“\tchild ppid = %d\n”,getppid());//取得目前进程的父进程ID exit(EXIT_SUCCESS); }else{ } exit(EXIT_SUCCESS); puts(“in parent”); printf(“\tparent pid = %d\n”,getpid()); printf(“\tparent ppid = %d\n”,getppid()); 13. 阅读下列程序: /* usage of kill,signal,wait */ #include #include #include #include int flag; void stop(); //该函数是自定义的一个 single()触发的自定义函数 int main(void) { int pid1,pid2; //定义了两个进程号参数 signal(3,stop); //signal() 触发软中断 while((pid1=fork()) ==-1); //程序等待成功创建子进程事件的发生 if(pid1>0){ while((pid2=fork()) ==-1); if(pid2>0){ //当前进程为父进程,父进程发出两个中断信号Kill子进程 flag=1; sleep(5); } kill(pid1,16); kill(pid2,17); wait(0); //等待子进程死信号 wait(0); printf(“\n parent is killed\n”); //接收到子进程死信号后,杀死父进程 exit(EXIT_SUCCESS); }else{ //当前进程为子进程,则发送子进程Kill信号,杀死该子进程2 } flag=1; signal(17,stop); printf(“\n child2 is killed by parent\n”); exit(EXIT_SUCCESS); }else{ //当前进程为子进程,则发送子进程Kill信号,杀死该子进程1 } flag=1; signal(16,stop); printf(“\n child1 is killed by parent\n”); exit(EXIT_SUCCESS); void stop(){ //自定义函数,供signal()调用 } 编译并运行,等待或者按^C,分别观察执行结果并分析,注释程序主要语句。 flag有什么作用?通过实验说明。 每个进程(父进程,子进程)都有一个flag,起状态标志作用,flag=1时,表示进程在运行,flag=0,表示进程结束。 flag = 0; 14. 编写程序,要求父进程创建一个子进程,使父进程和子进程各自在屏幕上输出一些信息,但父进程的信息总在子进程的信息之后出现。(分别通过一个程序和两个程序实现) 代码如下: Ptest.c ---------------一个程序实现方案(fork()) #include #include #include main() { int p,i; while((p=fork())==-1); //创建子进程直至成功 if(p>0) { wait(0); printf("***\n"); printf("The parent process!\n"); printf("***\n"); exit(0); } else{ printf("***\n"); printf("The child process!\n"); printf("***\n"); sleep(3); exit(0); } } ///////////////////////////////////////////////////////////////////////////////////// Ptest2.c ------------------两个程序实现方案(execl()) #include #include #include int main(int argc,char *argv[]) { int p,i; while((p=fork())==-1); //创建子进程直至成功 if(p>0) { wait(0); printf("***\n"); printf("The parent process!\n"); printf("***\n"); exit(0); } else{ printf("***\n"); printf("The child process!\n"); execl("/home/xingkong/ptest22",argv[1],(char*)0); printf("***\n"); sleep(3); exit(0); } } ptest22.c #include #include int main(int argc,char *argv[]) { int i; printf("*****\nThis is two process\n*****\n"); for(i=0;i {
printf("parameter %d is:%s\n",i,argv[i]); }
return 0; }
15. 编写程序,要求父进程创建一个子进程,子进程执行shell命令find / -name hda* 的功能,
子进程结束时由父进程打印子进程结束的信息。执行中父进程改变子进程的优先级。
代码如下: #include
#include #include #include
#include #include
#include main(){ pid_t pid; int status; pid=fork(); if(pid>0){
//在父进程中设置子进程优先级 setpriority(PRIO_PROCESS,pid,15); //输出修改后的子进程的优先级
printf("the priority of son process is %d\n",getpriority(PRIO_PROCESS,pid)); }
//子进程执行代码 else{
execlp("find", "find", "/", "-name", "hda*", (char *)0); exit(127); }
/*使用waitpid()阻塞等待子进程结束,防止父进程过早的退出。子进程终止后,waitpid()返回
返回后,可以打印子进程已经终止的信息。*/ if ( (pid = waitpid(pid, &status, 0)) == -1) {
fprintf(stderr, "[parent] waitpid error: %s\n", strerror(errno)); exit(-1); }
fprintf(stdout, "child[%d] terminated\n", pid); exit(0); }
16. 编写程序,要求父进程创建一个子进程,子进程对一个50*50的字符数组赋值,由父进
程改变子进程的优先级,观察不同优先级进程使用CPU的时间。
代码如下:
#include #include #include #include #include
#include
void time_print(char *str,clock_t time){ long tps=sysconf(_SC_CLK_TCK);
printf("%s:%6.2f secs",str,(float)time/tps); }
main(){ pid_t pid;
clock_t start,end;
struct tms t_start,t_end; pid=fork();
start=times(&t_start); if(pid>0){
//在父进程中设置子进程优先级 setpriority(PRIO_PROCESS,pid,20); //输出修改后的子进程的优先级
printf("the priority of son process is%d",getpriority(PRIO_PROCESS,pid)); }
//子进程执行代码 else{
int i,j,shu[50][50]; for(i=0;i<50;i++) for(j=0;j<50;j++)
shu[i][j]=i+j;
system("grep the /usr/*/*/* >/dev/null 2>/dev/null"); }
end=times(&t_end);
time_print("\nelapsed",end-start); printf("\nparent time");
time_print("\tuser CPU",t_end.tms_utime); time_print("\tsys CPU",t_end.tms_stime); printf("\nchild time");
time_print("\tuser CPU",t_end.tms_cutime); time_print("\tsys CPU",t_end.tms_cstime); printf("\n"); exit(0); }
17. 编写一个程序,模拟实现一个简单的进程管理子系统,它由进程建立模块、进程撤销模
块、进程控制表组成。该子系统通过循环显示“please input new command”接收新进程,根据用户键入内容(命令)启动新进程,然后不等待新进程结束就显示“please input new command” 接收新进程。建立和撤销进程时修改进程控制表。
18. 查阅Linux系统中struct task_struct (有很多结构体类型的指针,至少写出三级指针)
的定义,说明每项成员的作用。(熟记该嵌套结构体) Struct Task_struct{
*-------------------------------------- 新的结构体定义{
*--------------------------------------- 新的结构体定义 } } 一级指针
每一个进程都有一个进程描述符,具体是task_struct结构体存储相关的信息. struct task_struct {
//这个是进程的运行时状态,-1代表不可运行,0代表可运行,>0代表已停止。 volatile long state; /*
flags是进程当前的状态标志,具体的如:
0x00000002表示进程正在被创建;
0x00000004表示进程正准备退出;
0x00000040 表示此进程被fork出,但是并没有执行exec;
0x00000400表示此进程由于其他进程发送相关信号而被杀死 。 */
unsigned int flags;
//表示此进程的运行优先级 unsigned int rt_priority;
//list_head结构体
struct list_head tasks;
//mm_struct 结构体,该结构体记录了进程内存使用的相关情况 struct mm_struct *mm;
/* 接下来是进程的一些状态参数*/ int exit_state;
int exit_code, exit_signal;
//这个是进程号 pid_t pid;
//这个是进程组号 pid_t tgid;
//real_parent是该进程的“亲生父亲”,不管其是否被“寄养”。 struct task_struct *real_parent;
//parent是该进程现在的父进程,有可能是“继父” struct task_struct *parent;
/*这里children指的是该进程孩子的链表,可以得到所有孩子的进程描述符,但是需使用list_for_each和list_entry,list_entry其实直接使用了container_of,详情请参考*/ struct list_head children;
//同理,sibling该进程兄弟的链表,也就是其父亲的所有孩子的链表。用法与children相似。
struct list_head sibling;
/*这个是主线程的进程描述符,也许你会奇怪,为什么线程用进程描述符表示,因为linux并没有单独实现线程的相关结构体,只是用一个进程来代替线程,然
后对其做一些特殊的处理。*/
struct task_struct *group_leader;
//这个是该进程所有线程的链表。 struct list_head thread_group;
//顾名思义,这个是该进程使用cpu时间的信息,utime是在用户态下执行的时间,stime是在内核态下执行的时间。 cputime_t utime, stime;
//下面的是启动的时间,只是时间基准不一样。 struct timespec start_time; struct timespec real_start_time;
//comm是保存该进程名字的字符数组,长度最长为15,因为TASK_COMM_LEN为16。
char comm[TASK_COMM_LEN];
/* 文件系统信息计数*/
int link_count, total_link_count;
/*该进程在特定CPU下的状态*/ struct thread_struct thread;
/* 文件系统相关信息结构体*/ struct fs_struct *fs;
/* 打开的文件相关信息结构体*/ struct files_struct *files;
/* 信号相关信息的句柄*/
struct signal_struct *signal; struct sighand_struct *sighand;
/*这些是松弛时间值,用来规定select()和poll()的超时时间,单位是纳秒nanoseconds */
unsigned long timer_slack_ns;
unsigned long default_timer_slack_ns; };
二级结构体 及对应的三级结构体(2.代表二级的,3.代表三级的)
2. struct mm_struct {//该结构体记录了进程内存使用的相关情况 int count;
//分别为代码段、数据段的首地址和终止地址。
pgd_t * pgd;// 为指向进程页目录表的指针。 unsigned long context;// 是进程上下文的地址
unsigned long start_code, end_code, start_data, end_data; // start_stack 是进程堆栈的首地址
unsigned long start_brk, brk, start_stack, start_mmap; //分别为参数区、环境变量区的首地址和终止地址
unsigned long arg_start, arg_end, env_start, env_end; unsigned long rss, total_vm, locked_vm; unsigned long def_flags;
struct vm_area_struct * mmap; struct vm_area_struct * mmap_avl; struct semaphore mmap_sem;
};
3.struct vm_area_struct {
struct mm_struct * vm_mm;//指向进程的mm_struct结构体
//虚存空间的首地址和末地址后第一个字节的地址
unsigned long vm_start; unsigned long vm_end;
//通过vm_next指针指向下一个vm_area_struct结构
struct vm_area_struct *vm_next;
//虚存区域的页面的保护特性
pgprot_t vm_page_prot; unsigned long vm_flags;
// vm_flags指出了虚存区域的操作特性: // VM_READ // VM_WRITE
// VM_EXEC // VM_SHARED
虚存区域允许读取 虚存区域允许写入 虚存区域允许执行 虚存区域允许多个进程共享
// VM_GROWSDOWN 虚存区域可以向下延伸 // VM_GROWSUP // VM_SHM
虚存区域可以向上延伸
虚存区域是共享存储器的一部分
// VM_LOCKED 虚存区域可以加锁
// VM_STACK_FLAGS 虚存区域做为堆栈使用
/*vm_avl_hight(树高)、vm_avl_left(左子节点)、vm_avl_right
(右子节点)三个成员来实现AVL树*/
short vm_avl_height;
struct vm_area_struct * vm_avl_left; struct vm_area_struct * vm_avl_right;
/* vm_next_share和vm_prev_share,把有关vm_area_struct
结合成一个共享内存时使用的双向链表*/
struct vm_area_struct *vm_next_share; struct vm_area_struct **vm_pprev_share; struct vm_operations_struct * vm_ops; unsigned long vm_pgoff;
struct file * vm_file;
unsigned long vm_raend; void * vm_private_data;
};
2. struct list_head {
struct list_head *next, *prev; };
在 Linux 内核链表中,需要用链表组织起来的数据通常会包含一个struct list_head 成员。这种通用的链表结构避免了为每个数据项类型定义自己的链表的麻烦。
两个指针分别指向双向链表的两个不同指向的指针 list_head的若干常用操作:
链表的插入删除合并操作:函数list_add(),list_add_tail(),list_del(),list_move(),list_move_tail(),list_empty(),list_splice();
2. struct fs_struct {
atomic_t count; 计数器 rwlock_t lock; 读写锁 int umask;
struct dentry * root, * pwd, * altroot;//根目录("/"),当前目录以及 替换根目录
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt; };
3. struct dentry {
atomic_t d_count; //目录项对象使用计数器,可以有未使用态,使用态和负状态
unsigned int d_flags; //目录项标志
struct inode * d_inode;// 与文件名关联的索引节点 struct dentry * d_parent;// 父目录的目录项对象 struct list_head d_hash; //散列表表项的指针 struct list_head d_lru; //未使用链表的指针
struct list_head d_child; //父目录中目录项对象的链表的指针
struct list_head d_subdirs; //对目录而言,表示子目录目录项对象的链表
struct list_head d_alias; //相关索引节点(别名)的链表
int d_mounted; //对于安装点而言,表示被安装文件系统根项 struct qstr d_name; //文件名
unsigned long d_time; /* used by d_revalidate */ struct dentry_operations *d_op; //目录项方法 struct super_block * d_sb;// 文件的超级块对象 vunsigned long d_vfs_flags;
void * d_fsdata; //与文件系统相关的数据
unsigned char d_iname [DNAME_INLINE_LEN]; //存放短文件名 };
3. struct vfsmount {
struct list_head mnt_hash; /* hash table list */ struct vfsmount *mnt_parent; /* parent filesystem */
struct dentry *mnt_mountpoint; /* dentry of this mount point */ struct dentry *mnt_root; /*dentry of root of this fs */
struct super_block *mnt_sb; /* superblock of this filesystem */ struct list_head mnt_mounts; /* list of children */ struct list_head mnt_child; /* list of children */ atomic_t mnt_count; /* usage count */ int mnt_flags; /* mount flags */
char *mnt_devname; /* device file name */
struct list_head mnt_list; /* list of descriptors */
struct list_head mnt_fslink; /* fs-specific expiry list */ struct namespace *mnt_namespace; /* associated namespace */
};
2. struct files_struct {
atomic_t count; 使用该表的进程数 struct fdtable *fdt; struct fdtable fdtab;
spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd; 数值最小的最近关闭文件的文件描述符,下一个可用的文件描述符
struct embedded_fd_set close_on_exec_init; 执行exec时需要关闭的文件描述符初值集合
struct embedded_fd_set open_fds_init; 文件描述符的屏蔽字初值集合 struct file * fd_array[NR_OPEN_DEFAULT]; 默认打开的fd队列 };
3. struct fdtable {
unsigned int max_fds;
struct file ** fd; //指向打开的文件描述符列表的指针, 开始的时候指向fd_array,当超过max_fds时,重新分配地址
fd_set *close_on_exec;// 执行exec需要关闭的文件描述符位图(fork,exec即不被子进程继承的文件描述符) fd_set *open_fds; // 打开的文件描述符位图 struct rcu_head rcu; struct fdtable *next; };
3.struct file {
union {
struct list_head fu_list; 文件对象链表指针linux/include/linux/list.h struct rcu_head fu_rcuhead; RCU是Linux 2.6内核中新的锁机制 } f_u;
struct path f_path; 包含dentry和mnt两个成员,用于确定文件路径 const struct file_operations *f_op; 与该文件相关联的操作函数 atomic_t f_count; 文件的引用计数(有多少进程打开该文件) unsigned int f_flags; 对应于open时指定的flag
mode_t f_mode; 读写模式:open的mod_t mode参数 off_t f_pos; 该文件在当前进程中的文件偏移量
struct fown_struct f_owner; 该结构的作用是通过信号进行I/O时间通知的数据。
unsigned int f_uid, f_gid; 文件所有者id,所有者组id
struct file_ra_state f_ra; 在linux/include/linux/fs.h中定义,文件预读相关
unsigned long f_version; #ifdef CONFIG_SECURITY void *f_security; #endif
/* needed for tty driver, and maybe others */ void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */ struct list_head f_ep_links; spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; };
认识和体会:
1. 对进程属性有所认知,通过学习,对子进程和父进程以及INIT的关系更加清晰。 2. 进程的创建,fork()可以产生一个新的子进程,会复制父进程的数据代码,但进程ID是
不同的(及内存空间不同)。
3. 用exec函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID。
exec名下是由多个关联函数组成的一个完整系列, 头文件
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, ..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]);
带l 的exec函数:execl,execlp,execle,表示后边的参数以可变参数的形式给出且都以一个空指针结束。
带 p 的exec函数:execlp,execvp,表示第一个参数path不用输入完整路径,只有给出命令名即可,它会在环境变量PATH当中查找命令。 不带 l 的exec函数:execv,execvp表示命令所需的参数以char *arg[]形式给出且arg最后一个元素必须是NULL。
带 e 的exec函数:execle表示,将环境变量传递给需要替换的进程。 4. 进程等待 wait() waitpid().
5. 还了解了信号处理signal()和优先权的获取与设置getpriority() setpriority() nice(),
以及进程正常结束exit().
本文来源:https://www.wddqw.com/doc/ebb25eee88d63186bceb19e8b8f67c1cfad6eef2.html