Internet Application 3:爷爷-爸爸-你的神秘旅程

fork1.c

1
2
3
4
5
6
7
8
9
10
11
#include<sys/types.h>
#include<unistd.h> //此头文件包含了fork()
#include<stdio.h>
#include<stdlib.h>

int main(void){
pid_t t;
t=fork();
printf("fork returned %d\n",t);
exit(0);
}
1
2
3
[email protected]:~/fork$ ./fork1
fork returned 1224
[email protected]:~/fork$ fork returned 0

(1)为什么同样是c语言,这个例子不可以直接在c-free里编译运行呢?

答:fork()这个函数来自Unix的函数库,所以在Windows下不支持,Windows上创建进程一般使用CreateProcess函数。

(2)fork()的返回值传给了t,返回值是怎样的?

1)在父进程中,fork返回新创建子进程的Process ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;

(3)出现错误,返回负值的原因

可能有两种原因:
​ 1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。

(4)为什么多次运行的打印顺序不一样?

答:和Java多线程一样,创建新进程成功后,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

(5)为什么我gcc完老师的代码报了一堆错?

太久不碰c语言我们可能忘了编译器的提示会有两种错误(error)和警告(warning),警告是可以无视的,不会影响运行的。比如老师的代码里使用了I/O相关的函数但是没有在头部引入,gcc就会给你甩个warning然后自己默默enable这个函数…

fork11.c — 拓展

1
2
3
4
5
6
7
8
9
10
11
12
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main(void){
pid_t t1,t2;
t1=fork(); //这句话之后,共有两个进程:父进程、父进程产生的子进程。
t2=fork(); //这句话会被上边的两个进程都执行,所以又产生了两个新的进程:父进程产生的第二个子进程、父进程产生的第一个子进程的子进程。
printf("fork returned %d %d\n",t1,t2);
exit(0);
}
1
2
3
4
5
[email protected]:~/fork$ ./fork11
fork returned 1237 1238
[email protected]:~/fork$ fork returned 1237 0
fork returned 0 1239
fork returned 0 0

(1)在多添加了一个fork()之后,其实一共产生了4个进程,分别是爷爷 - 爸爸/叔叔 - 你。具体流程参见代码注释。

(2)不妨想一想输出结果的四行分别对应哪个进程,我相信初次见面一定会发现诡异的地方的,欢迎讨论。

Hint:执行fork()时,并不是从#include处开始复制代码。详细的原因可在本页面某处找到:)

(3)那怎么用两个fork()实现爷爷 - 爸爸 - 你,这样的流程来得到能直接链表串起来的三个进程啊?

答案:加个判断,只让子进程产生子进程,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

int main(void){
pid_t t1,t2;
t1=fork();
if(t1==0)
t2=fork();
printf("fork returned %d %d\n",t1,t2);
exit(0);
}

fork2.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>

int main(void){
pid_t t;

printf("Original program, pid=%d\n", getpid());
t=fork();
if(t==0){
printf("in the child process, pid=%d,ppid=%d\n",getpid(),getppid());
}else{
printf("in parent, pid=%d,fork returned=%d\n",getpid(),t);
}
}
1
2
3
4
[email protected]:~/fork$ ./fork2
Original program, pid=1259
in parent, pid=1259,fork returned=1260
[email protected]:~/fork$ in the child process, pid=1260,ppid=1

(1)为什么最后ppid=1?

答:因为此时父进程的main函数退出了,父进程死亡了,子进程(pid=1260)没有了父进程,这是操作系统不允许的,所以系统把子进程的ppid置为1(INIT)。

exec1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<unistd.h>
#include<stdio.h>

int main(void){
char *arg[] = {"/bin/ls",0};

if(fork() == 0){
printf("in child process: \n");
execv(arg[0], arg);

printf("I will never be called \n");
}
printf("execution continues in parent process.\n");

}
1
2
3
4
5
6
[email protected]:~/fork$ ./exec1
execution continues in parent process.
[email protected]:~/fork$ in child process:
exec1 exec2.c file.hole fork111 fork1.c lseek1 out.out readwrite2
exec1.c exec3 fork1 fork111.c fork2 lseek1.c readwrite1 readwrite2.c
exec2 exec3.c fork11 fork11.c fork2.c out1.out1 readwrite1.c

(1)为什么子进程不执行printf(“I will never be called \n”);?

答:execv()在创建进程时与fork()不同,执行了execv()的命令之后子进程就已经被替换,脱胎换骨不在继续往下走了。

(2)execv()的参数列表到底是怎样的?

一开始看到代码里的参数,我是这样想的:第一个参数指明路径,第二个参数是要执行的命令和命令需要的参数,然后运行的时候,根据第一个参数找到命令所在的路径再执行,就像在cmd里运行javac,系统会先去环境变量里找到javac.exe所在的路径,在执行javac.exe那样,是不是看起来很有道理???然而并不是这样的。

我们找到execv()的函数原型:

1
int execv(const char *progname, char *const argv[]);   //#include <unistd.h>

事实是,第一个参数progname是被执行的命令(应用程序)的名字,且这个名字是需要带路径的,第二个参数argv是传递给命令(应用程序)的参数列表,为了保证这个参数列表数组的确是要执行的命令所需要的(我瞎猜的),我们必须把参数列表数组的第一个元素写成命令(应用程序)的名字。

所以其实Linux直接通过带路径的progname找到应用程序,在把argv的参数丢给应用程序,顺便检查一下argv里的第一个元素和应用程序的名字匹不匹配。

如果应用程序所需要的参数列表为空,那我们可以直接写execv(“/bin/ls”, NULL);

lseek1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
#define FILE_MODE 0777

int main(void){
int fd;

if((fd=creat("file.hole",FILE_MODE)) < 0)
{ printf("creat error\n");
exit(1);}

if(write(fd,buf1,10)!=10){
printf("buf1 write error\n");
exit(1);}

if(lseek(fd,40,SEEK_SET) == -1){
printf("lseek error\n");
exit(1);
}

if(write(fd,buf2,10)!=10){
printf("buf2 write error\n");
exit(1);}
exit(0);
}
1
2
3
4
5
6
7
8
[email protected]:~/fork$ ls -l file.hole
-rw-r--r-- 1 student student 50 Mar 25 17:38 file.hole
[email protected]:~/fork$ od -c file.hole
0000000 a b c d e f g h i j \0 \0 \0 \0 \0 \0
0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
0000040 \0 \0 \0 \0 \0 \0 \0 \0 A B C D E F G H
0000060 I J
0000062

(1)open函数:

1.int open(const char pathname, int flags);
2.int open(const char
pathname, int flags, mode_t mode);

O_CREAT:如果文件不存在则创建文件,初次创建必须写;
O_TRUNC:如果这个文件已经存在并且为可读可写/只写,这个文件内容将被清零;

mode只有初次才用,用来规定权限。

(2)creat函数:

int creat(const char *pathname, mode_t mode);

creat() is equivalent to open() with flags equal to O_CREAT|O_WRONLY|O_TRUNC.

readwrite1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<fcntl.h>
#include<unistd.h>

int main(void){

char quit = '.';
char buf[10];
int fd;
if((fd = open("out.out",O_RDWR | O_CREAT,0)) == -1)
printf("error in opening \n");
while(buf[0]!=quit){
read(0,buf,1);
write(fd,buf,1);
write(1,buf,1);
}
close(fd);
}

(1)write()

函数定义:ssize_t write (int fd, const void * buf, size_t count);

函数说明:write()会把参数buf所指的内存写入count个字节到参数放到所指的文件内。

返回值:如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。

NOTE:write()函数从buf写数据到fd中时,若buf中数据无法一次性读完,那么第二次读buf中数据时,其读位置指针(也就是第二个参数buf)不会自动移动,需要程序员编程控制,而不是简单的将buf首地址填入第二参数即可。如可按如下格式实现读位置移动:write(fp, p1+len, (strlen(p1)-len)。 这样write第二次循环时变会从p1+len处写数据到fp, 之后的也由此类推,直至(strlen(p1)-len变为0。

(2)read()

函数定义:ssize_t read(int fd, void * buf, size_t count);

函数说明:read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。

返回值:返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据。若参数count 为0, 则read()不会有作用并返回0。

NOTE:read时fd中的数据如果小于要读取的数据,就会引起阻塞。

(3)Linux所有的IO都是靠文件,所以用到了两个特殊的file descriptor

0 - 键盘

1 - 屏幕

signal1.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<signal.h>
void signalRoutine(int);

int main(void){
printf("signal processing demo program\n");
while(1){
signal(SIGINT,signalRoutine);
}

}
void signalRoutine(int dummy)
{
printf("signalRoutine called [%d] \n",dummy);
exit(0);
}
1
2
3
[email protected]:~/fork$ ./signal1
signal processing demo program
^CsignalRoutine called [2]

(1)signal()

1
2
3
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

第一个参数signum:指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号;  

第二个参数handler:描述了与信号关联的动作;

在本实例中测试signal interrupt靠Ctrl+C来实现,SIGINT对应2。

Scan the QR code to add me on Wechat