OS实验二:进程控制和系统调用

实验二:进程控制和系统调用

OS实验二:进程控制和系统调用_第1张图片

1.实验要求

//本程序达到所有要求

具体要求:

  1. 程序调用 fork 创建子进程 ;
  2. 子进程调用 execvp() 执行其他程序 ;
  3. 调用 exit 终止进程 ;
  4. 代码有注释,提交实验报告 。
    进一步要求:
  5. 使用 wait() 替代示例代码中的 sleep 系统调用 ;
  6. 使用 C 程序调用 execvp ;
  7. 将实验 1 与实验 2 结合:在子进程中使用 execvp 运行实验一脚本代码 。

2.设计思路

程序只有一个函数:主函数。
在主函数中:
调用一次 fork 函数创建子进程。根据 fork 函数返回的 pid,
用一个 if - else 条件判断语句来将父进程和子进程区别开。
创建的子进程( pid == 0 ):调用 execvp 执行实验 1 shell 脚本代码。
创建的父进程( pid >0 ):调用 wait 等待子进程结束,然后exit。

3.实验结果截图

1.在 shell 中编译并运行程序
2.输出结果:子进程结束后,程序 exit

4.实验中遇到问题及解决办法

1.如何使用 exec 族的其他函数执行实验 1 代码
查找资料后,决定使用 execvp 。
execve
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
①文件路径 ②传递参数给执行文件 ③传递环境变量给执行文件
执行成功函数无返回值,执行失败返回 -1 。
execvp
int execvp(const char *filename, char *const argv[ ]);
①文件路径 ②传递参数给执行文件
执行成功函数无返回值,执行失败返回 -1 。

2.wait 系统调用用法
pid_t wait (int * status);
wait()会暂时停止目前进程的执行,直到有信号来到或子进程结束。
如果在调用函数时子进程已经结束,则会立即返回子进程结束状态值。
函数返回值为子进程的 pid 或 -1 (错误),子进程的结束状态值会由参数status返回。
如果不在意结束状态值,则参数 status 可以设成NULL。

5.实验代码

详见文末。

6.exec库函数

// 在实验报告中给出 exec 族函数的定义与区别
语法:
所需头文件:#include
函数说明:执行文件
函数原型:
int execl(const char *path, const char *arg, …)
int execv(const char *path, char *const argv[])
int execle(const char *path, const char *arg, …, char *const envp[])
int execve(const char *path, char *const argv[], char *const envp[])
int execlp(const char *file, const char *arg, …)
int execvp(const char *file, char *const argv[])
函数返回值:
成功:函数不会返回;
出错:返回-1,失败原因记录在error中。

exec函数族关系:
真正的系统调用只有 execve,其他 5 个都是库函数,最终都会调用 execve。
OS实验二:进程控制和系统调用_第2张图片

区别:

  1. 查找方式:上面所列前 4 个函数的查找方式都是完整的文件目录路径,而最后 2个函数(也就是以 p(path)结尾 的两个函数)可以只给出文件名,系统就会自动从环境变量 “$PATH” 所指出的路径中进行查找。
  2. 参数传递方式:exec函数族的参数传递有两种方式,一种是逐个列举的方式,而另一种则是将所有参数整体构造成指针数组进行传递。
    在这里参数传递方式是以函数名的第5位字母来区分的:
    字母为 “l”(list)的表示逐个列举的方式;
    字母为 “v”(vertor)的表示将所有参数整体构造成指针数组传递,然后将该数组的首地址当做参数传给它,数组中的最后一个指针要求是NULL。
  3. 环境变量:exec 函数族使用了系统默认的环境变量,也可以传入指定的环境变量。
    这里以 “e”(environment)结尾 的两个函数 execle、execve 就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所有环境变量。

7.三对系统调用函数的区别

fork v.s vfork

fork :创造的子进程是父进程的完整副本,采用copy-on-write 技术,复制父亲进程的资源,包括内存的内容 task_struct
内容,父子进程执行次序不定。 调用成功返回两个值:父进程的 pid 以及子进程的pid(为0),调用失败返回 -1 。
父进程与子进程资源独立。

vfork :创建的子进程与父进程共享数据段,而且由 vfork 创建的子进程将先于父进程运行。 创建子进程后,父进程阻塞,直到子进程执行
exec / exit 。 子进程不是真正意义上的进程,缺少独立内存资源。

区别:
1.fork :子进程拷贝父进程的数据段,代码段
vfork:子进程与父进程共享数据段

2.fork:父子进程的执行次序不确定
vfork:保证子进程先运行,在调用exec / exit 之前与父进程数据是共享的,在它调用exec / exit 之后父进程才可能被调度运行。

3.vfork 保证子进程先运行,在调用exec / exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

exit v.s _exit

exit():
#include void exit(int status);

  1. 调用 atexit 注册的函数:按 atexit 注册时相反的顺序调用所有由它注册的函数,这使得可以指定在程序终止时执行清理动作。
  2. cleanup :关闭所有打开的流,这将导致写所有被缓冲的输出,删除用tmpfile 函数建立的所有临时文件。
  3. 调用 _exit 函数终止进程。

_exit(): include void _exit(int status);

  1. Any open file descriptors belonging to the process are closed
  2. any children of the process are inherited by process 1, init
  3. the process’s parent is sent a SIGCHLD signal

区别:

  1. exit 定义在 stdlib.h 中,调用实施调用库里用户状态结构的清除工作,而且调用用户自定义的清除程序。
    _exit 定义在 unistd.h 中,只为进程实施内核清除工作,不关闭文件,不清除输出缓存,也不调用出口函数。

  2. _exit 直接使进程停止运行,清除其使用的内存空间,销毁其在内核中的数据结构。
    exit 在 _exit 基础上作了一些包装,在执行退出之前加了若干道工序。
    e.g exit在调用之前要检查文件的打开情况,将文件缓冲区的内容写回文件,即:清理 I/O 缓冲。

fork v.s clone

fork :见上。

clone :Linux上创建线程一般使用的是pthread库,clone 是 Linux 提供的创建线程的系统调用。clone
也可以创建进程。
可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等。
clone 函数结构: int clone(int (*fn)(void *), void *child_stack, int flags,
void *arg); fn:函数指针,指向一个函数体,即想要创建进程的静态程序; child_stack:给子进程分配系统堆栈的指针;
flags:要复制资源的标志,描述你需要从父进程继承那些资源(复制/共享); arg:传给子进程的参数,一般为(0);

区别:
1.调用方式不同。clone 调用需要传入一个函数,该函数在子进程中执行。
2.clone 不再复制父进程的栈空间,而是自己创建一个新的。

8.fork返回两次理解

当程序执行到 fork 函数时,os 创建子进程,并在进程表中相应地建立一个新的表项。新进程和原有进程的可执行程序是同一个程序,上下文和数据绝大部分是原进程(父进程)的拷贝,但它们是两个相互独立的进程。

此时,在父、子进程的上下文中都声称,这个进程目前执行到 fork 调用即将返回。(此时子进程不占有 CPU,子进程的 pc
不是真正保存在寄存器中,而是作为进程上下文保存在进程表中的对应表项内。)

若父进程继续执行,os 实现 fork 函数,使得该调用在父进程中返回 刚刚创建的子进程的 pid 。
若子进程继续执行,上下文被换入,占据 CPU,os 对 fork 的实现使得该调用 在子进程中返回 0 。
「 父子进程是并发运行的独立进程,内核能以任意方式交替执行它们的逻辑控制流中的指令。」
所以,fork 的两次返回实际上是因为两个独立进程的执行。

9.五个常用系统调用

  1. 获取进程识别号—— getpid 、getppid
    #include
    #include
    pid_t getpid(void);
    //获取进程标识号。这可以作为一个独一无二的临时文件名。
    pid_t getppid(void);
    //获取父进程标示号。

  2. 向进程发送信号 —— kill
    #include
    #include
    int kill(pid_t pid, int sig);
    //kill系统调用可以用来向任何进程组或进程发送信号。
    如果pid为正,则信号发给 pid 指定的进程。
    如果pid为 0, 则信号发送给调用进程所在进程组的所有进程。
    如果pid为-1,则信号发送给调用进程有权限发送信号的所有进程,除了进程1(init)。
    如果pid小于-1,则信号发送给进程组为-pid 内的所有进程。
    如果sig为0,那么没有发送信号,但是错误检查会进行,这个可以用来检查进程号或进程组号存在与否。
    //如果成功(至少一个发送了一个信号),返回0,否则,返回-1,errno被设置。

  3. 共享内存控制 —— shmctl
    #include
    #include
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    //shmctl执行一个cmd指定的操作在shmid指定的共享内存上。
    参数buf是一个shmid_ds结构体指针,定义在文件中。
    cmd有三个可能的取值:
    IPCSTAT:复制shmid关联的shmidds结构内容到buf指向的空间,调用者必须有对共享内存的读权限。
    IPCSET:将buf指向结构体中成员的值写入到与shmid关联的shmidds结构体,同事更新shmctime。
    下面的成员可以被改变:shmperm.uid, shmperm.gid和shmperm.mode的最低9个有效位。必须有足够权限。
    IPC_RMID:删除共享内存段。必须有足够权限。只有没有进程与这个共享内存连接的时候才可以删除。
    执行错误返回-1,errno被设置。

  4. 从一个文件描述符中读取内容 —— read
    #include
    ssizet read(int fd, void *buf, sizet count);
    // read 从文件描述符fd指向的文件读取 count 个字节存放在 buf 缓冲区中。
    read从当前文件指针指向的位置读取,如果在文件末尾,返回0,表示没有读取字节。
    执行成功返回读取的字节个数,错误情况下返回-1,errno被设置。

  5. 向一个文件描述符中写入内容 —— write
    #include
    ssizet write(int fd, const void *buf, sizet count);
    // write 向文件描述符 fd 指向的文件写入 buf 缓冲区指向的 count 个字节。
    写入的字节数可能会少于 count ,如:存储空间不足或系统调用 setrlimit 设置了 RLIMIT_FSIZE 资源限制,或者 write 被信号中断。
    执行成功返回写入的字节个数,0表示没有写入,否则返回-1,errno被设置。

10. 参考资料

  1. Shell 编译 C 文件
    gcc b.c - > a.out(默认)
    gcc -o b.out b.c - > b.out
    gcc -o b b.c - > b
  2. Shell 运行 C 文件
    ./a.out
    ./b.out
    ./b
  3. execve, execlp, execvp, execle比较 - sqx2011的专栏 - CSDN博客
  4. fork如何做到返回两次 - barfoo的专栏 - CSDN博客
  5. linux常用系统调用简介 - gwq5210的专栏 - CSDN博客
  6. exec函数 - guoping16的专栏 - CSDN博客

代码

/* function: 
 * A demo of usage of execvp, child process will execute "bash experiment1.sh"
 * And the parent process will wait the child to exit.
 */
#include 
#include 
#include 
#include 
#include 

char *argv[ ]={"/bin/bash","experiment1.sh",NULL}; 
 //运行实验一

int main(void)
{
	pid_t pid ;

	if( (pid= fork()) < 0)	//wrong
	{
		printf("fork error\n");
		exit(0);
	}
        else if(pid != 0)			//parent
	{
                pid = wait(NULL);  //等待子进程执行完

                exit(0);
	}
	else 	//child
	{
		printf("--------------\n");
		printf("This is the first child!\n");


		if (execvp("/bin/bash",argv) < 0)	
		//execute "bash experiment1.sh"
			printf("execve error\n");

	}
       
        printf("This is the end of this program but u will never see this !\n");
        
	return 0;
} 



你可能感兴趣的:(OS实验)