浅浅记录一下做实验遇到的一些问题
1.刚开始做实验的时候直接在process.c上修改,最后发现无法保存,才通过查看文件权限发现自己所在的用户组只有阅读权限,通过执行下面命令可以解决:
sudo vim process.c
2.当对process.c进行修改。并通过gcc -o test process.c编译时,出现报错:
后来发现Permission denied出现的原因的是:没有权限进行读、写、创建文件、删除文件等操作。
通过加上sudo命令可以解决:
sudo gcc -o test process.c
3.为了实验操作的方便,最好还是不要在process.c文件上直接修改,可以先通过下面命令,建立副本文件process_copy.c ,并修改其权限以方便使用。
sudo cp process.c process_copy.c
sudo chmod 777 process_copy.c
5.process_copy.c 中的mian函数如下:
int main(int argc, char * argv[])
{
pid_t father,son1,son2,tmp1,tmp2;
tmp1 = fork(); //父进程创建新的进程
if(tmp1 == 0) //子进程son1
{
/* son1 */
son1 = getpid();
printf("I am son1!\r\n");
printf("The son1's pid:%d\r\n",son1);
cpuio_bound(10,3,2);
printf("Son1 is finished!\r\n");
}
else if(tmp1 > 0) //父进程father
{
son1 = tmp1;
tmp2 = fork(); //父进程再次创建新进程
if(tmp2 == 0) //子进程son2
{
/* son2 */
son2 = getpid();;
printf("I am soni2!\r\n");
printf("The son2's pid:%d\r\n",son2);
cpuio_bound(5,1,2);
printf("Son2 is finished!\r\n");
}
else if(tmp2 > 0) //父进程father
{
son2 = tmp2;
father = getpid();
printf("The father get son1's pid:%d\r\n",tmp1);
printf("The father get son2's pid:%d\r\n",tmp2);
wait((int*)NULL); //父进程创建了son1、son2两个子进程,因此这里有两个wait(),即父进程要等到两个子进程都退出后自己才会退出
wait((int*)NULL);
printf("Now is the father's pid:%d\r\n",father);
}
else
printf("Creat son2 failed!\r\n");
}
else
printf("Creat son1 failed!\r\n");
return 0;
}
4.fork()、wait()和exit()函数
当使用了fork()函数,为什么程序末尾要有wait()函数出现?
父进程通过fork()创建了子进程,当子进程运行结束后通过exit系统调用将自己的返回状态提交给内核,而父进程通过wait得到保存在内核中的子进程返回状态,然后将子进程的PCB回收。如果父进程没有调用wait,则子进程运行结束后,他的返回状态不能被父进程接收,PCB表也不能被回收,也就成为了僵尸进程,当僵尸进程过多时,系统将无可用pid分配给新进程,导致进程加载失败。
总结来说,使用了fork()函数,程序末尾出现wait()函数是为了防止僵尸进程的出现,进而防止僵尸进程过多导致进程加载失败。
“孤儿进程”和“僵尸进程”的概念:
当一个进程由于某种原因停止时,内核并不是直接将为它保存的状态信息从内核移除,相反,进程会一直被保持在一种已经终止的状态,直到被它的父进程回收,当父进程回收已经终止的子进程时(即调用wait),内核会将子进程的退出状态传递给父进程,然后抛弃已经终止的进程,从此刻开始,这个进程才会消失,一个子进程结束但是还没有被父进程回收的进程叫做”僵尸进程”,也可以说,当一个子进程运行结束,他的父进程没有调用wait,那么该进程就变成了僵尸进程,要是父进程先于子进程结束,那么它的子进程就会称为”孤儿进程”,最终会被PID为1的init进程收养。
fork()函数:
(1)在父进程中,fork返回新创建子进程的进程ID;
(2)在子进程中,fork返回0;
(3)如果出现错误,fork返回一个负值。
exit()函数:进程最后都是主动调用exit系统调用结束程序运行。
wait()函数:称为let_child_execute_first()更加直观。wait()函数的作用是阻塞父进程自己,直到任意一个子进程结束运行。只要调用了waiit系统调用,若没有子进程,wait会返回-1,若有子进程,该进程会被阻塞,当其某个子进程退出了,会将子进程退出的值返回给父进程,并将父进程唤醒。
最后总结:exit是由子进程调用的,表面上功能是使子进程结束运行并传递返回值给内核,本质上是内核在幕后会将进程除pcb以外的所有资源都回收。wait是父进程调用的,表面上功能是使父进程阻塞自己,直到子讲程调用exit结束运行,然后获得子进程的返回值,本质上是内核在幕后将子进程的返回值传递给父进程并会唤醒父进程,然后将子进程的pcb 回收。
4.current宏,是一个全局指针,指向当前进程的struct task_struct结构体,即表示当前进程。例如current->pid就能得到当前进程的pid,current-comm就能得到当前进程的名称。
5.进程状态的转换以及所调用的函数
6.进程由运行态转移至睡眠态的切换流程
sleep_on()函数将进程由运行态转移至睡眠态,即当一个进程调用sleep()函数,该进程会由运行态转移至睡眠态,该函数主要涉及三个指针操作:*p,tmp,和current,*p是等待队列头指针,存放着原等待任务的地址,tmp是函数堆栈上建立的临时指针,当一个进程调用sleep_on()函数时,*p会指向该进程,即该进程加入了等待队列,而tmp会指向原等待队列头指针,以下图举例,当task1调用sleep_on(),因为原等待队列为空,所以tmp为空指针,*p指向task1,即task1加入等待队列,,当task2调用sleep_on(),tmp指向task1(即原等待队列头部),*p指向task2,即task2加入等待队列,当task3调用sleep_on(),tmp指向task2(即原等待队列头部),*p指向task3,即task3加入等待队列。
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
// ……
tmp = *p;
// 仔细阅读,实际上是将 current 插入“等待队列”头部,tmp 是原来的头部
*p = current;
// 切换到睡眠态
current->state = TASK_UNINTERRUPTIBLE;
// 让出 CPU去执行别的进程
schedule();
// 唤醒队列中的上一个(tmp)睡眠进程。0 换作 TASK_RUNNING 更好
// 在记录进程被唤醒时一定要考虑到这种情况,实验者一定要注意!!!
if (tmp)
tmp->state=0;
}
7.进程调度函数schdule():
A.遍历所有的进程任务,看看是否有收到信号量,如果是则将进程的状态从中断状态修改为运行状态
B.遍历所有的进程任务,寻找时间片计数counter最大的任务。如果所有任务的剩余间片计数counter都小于0,则将所有任务的时间片计数counter都重增加“任务自己的优先级priority”,再遍历进程任务,找到时间片计数counter最大的任务
C.将时间片计数counter最大的任务,调用起来(switch_to)
void schedule(void)
{
int i,next,c;
struct task_struct ** p; //task_struct定义在sched.h文件中,即进程描述符,注意这里的p是一个二级指针,*p才是指向task_struct的指针。
/* check alarm, wake up any interruptible tasks that have got a signal */
//下面这段代码就是完成A功能
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) {
if ((*p)->alarm && (*p)->alarm < jiffies) {
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0;
}
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
{
(*p)->state=TASK_RUNNING; //进程的状态转换就是通过改变状态码实现,TASK_RUNNING定义在include/linux/sched.h
}
}
//NR_TASKS是系统允许的最大进程数
while (1) {
c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; //task数组存储着每一个进程描述符的地址
// 这段代码就是将任务数组从最后向前遍历,找到 counter 值最大的就绪态进程
while (--i) {
if (!*--p) continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c) /如果任务是在就绪/运行态,
c = (*p)->counter, next = i; 且运行时间片大于c,就让next指向这个进程
}
// 如果有 counter 值大于 0 的就绪态进程,则退出
//只要c不等于0,即找到了counter 值最大的就绪态进程,就会退出while循环,并执行下面的swich_to()指令
//而只有没有进程处于就绪/运行态或所有处于这一状态的进程的时间片都等于0,也就是时间片都用完了,c才会等于0,此时会执行下面产生新的时间片的程序
if (c) break;
// 如果没有:
// 所有进程的 counter 值除以 2 衰减后再和 priority 值相加,对于时间片用完了的情况,进程产生的新时间片就等于它的运行优先数priority
// 产生新的时间片
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
}
// 切换到 next 进程,即counter最大的进程
switch_to(next);
}
参考文档 内核中的current宏
参考文档 学习linux0.11-schedule函数
参考文档 linux0.11进程调度(运行态\就绪态\睡眠态之间的转换)
参考文档 操作系统实验3:进程运行轨迹的跟踪与统计
[参考文档] linux内核完全注释