分类: LINUX
2011-10-01 10:36:56
/*
*<阻塞读终端>
*从终端读数据再写回到终端
*阻塞读终端,因为从终端读,以换行,回车为结束标志,
*若无则阻塞,即一直等待
*=======================
*./a.out
*hello(回车)
*hello
*./a.out
*hello world(回车)
*hello world$ d
*bash:d:command not found
*========================
*第一次执行a.out的结果很正常,而第二次执行的过程有点特殊,现在分析一下:
*1. Shell进程创建a.out进程,a.out进程开始执行,而Shell进程睡眠等待a.out进程退出。
*2. a.out调用read时睡眠等待,直到终端设备输入了换行符才从read返回,read只读走10个字符,剩下的字符仍然保存在内核的终端设备输入缓冲区中。
*3. a.out进程打印并退出,这时Shell进程恢复运行,Shell继续从终端读取用户输入的命令,于是读走了终端设备输入缓冲区中剩下的字符d和换行符,把它当成一条命令解释执行,结果发现执行不了,没有d这个命令。
*==================================================================================================================
*如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例,如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK
*/
#include
#include
int main(void) {
char buf[10];
int n;
n = read(STDIN_FILENO, buf, 10);
if(n < 0) {
perror("read STDIN_FILENO");
exit(1);
}
n = write(STDOUT_FILENO, buf, n);
if(n < 0) {
perror("write STDOUT_FILENO");
exit(1);
}
return 0;
}
/*
*<非阻塞读终端>
*轮询操作
*/
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
int main(void) {
char buf[10];
int fd, n;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
if(fd < 0) {
perror("open /dev/tty");
exit(1);
}
tryagain:
n = read(fd, buf, 10);
if(n < 0) {
if(errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read /dev/tty");
exit(1);
}
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
/*
*<非阻塞读终端和等待超时>
*/
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"
int main(void) {
char buf[10];
int fd, n, i;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
if(fd < 0) {
perror("open /dev/tty");
exit(1);
}
for(i = 0; i < 5; i++) {
n = read(fd, buf, 10);
if(n >= 0)
break;
if(errno != EAGAIN) {
perror("read /dev/tty");
exit(1);
}
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
}
if(i == 5)
write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
else
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
/*
*fcntl改变文件flag标志
*fcntl函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志,而不必重新open文件。
*/
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
int main(void) {
char buf[10];
int flags, n;
flags = fcntl(STDIN_FILENO, F_GETFL); //获取已经打开的终端的标志
flags |= O_NONBLOCK; //追加属性
if(fcntl(STDIN_FILENO, F_SETFL, flags) == -1) { //设置属性
perror("fcntl");
exit(1);
}
tryagain:
n = read(STDIN_FILENO, buf, 10);
if(n < 0) {
if(errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read stdin");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
/*
*ioctl改变标准输出(终端)的大小
*ioctl用于向设备发控制和配置命令
*/
#include
#include
#include
#include
int main(void) {
struct winsize size;
if(isatty(STDOUT_FILENO) == 0)
exit(1);
if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0) {
perror("ioctl TIOCGWINSZ error");
exit(1);
}
printf("%d rows, %d columns\n",size.ws_row, size.ws_col);
return 0;
}
/*
*mmap把文件直接映射到内存,此时对文件的读写就直接可以用指针代替
*mmap可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址,对文件的读写可以直接用指针来做而不需要read/write函数。
*void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);
*off参数是从文件的什么位置开始映射,必须是页大小的整数倍;
*filedes是代表该文件的描述符。
*/
#include
#include
#include
int main(void) {
int *p;
int fd = open("hello", O_RDWR); //先建立好hello文件
if(fd < 0) {
perror("open hello");
exit(1);
}
p = mmap(NULL, 6, PROT_WRITE, MAP_SHARED, fd , 0);
if(p == MAP_FAILED) {
perror("mmap");
exit(1);
}
close(fd); //关闭fd,并不影响映射
p[0] = 0x30313233; //使用指针直接修改内存地址内容,不用read,write函数
munmap(p, 6); //取消映射
return 0;
}
/*
*dup创建一个新的文件描述符,使两个文件描述符指向同一个file结构体,使得file结构体的引用次数是2
*dup和dup2都可用来复制一个现存的文件描述符,使两个文件描述符指向同一个file结构体。
*如果两个文件描述符指向同一个file结构体,File Status Flag和读写位置只保存一份在file结构体中,并且file结构体的引用计数是2。
*如果两次open同一文件得到两个文件描述符,则每个描述符对应一个不同的file结构体,可以有不同的File Status Flag和读写位置
*/
#include
#include
#include
#include
#include
#include
int main(void) {
int fd, save_fd;
char msg[] = "This is a test\n";
//文件描述符0,1,2都标识tty
fd = open("dup_", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR); //创建dup_文件,fd=3,标识dup_文件
if(fd < 0) {
perror("open");
exit(1);
}
save_fd = dup(STDOUT_FILENO); //创建新的文件描述符save_fd=4,标识tty,tty结构被引用次数=2
dup2(fd, STDOUT_FILENO); //文件描述符1现在标识dup_文件,不再标识tty,dup_文件结构被引用次数=2
close(fd); //此时dup_文件被引用次数变为1,dup_文件并未被关闭,关闭的只是fd标识的这个引用
write(STDOUT_FILENO, msg, strlen(msg));//因为1标识dup_文件,所以向dup_文件写入
dup2(save_fd, STDOUT_FILENO); //1又被重新标识tty,此时dup_文件被关闭
write(STDOUT_FILENO, msg, strlen(msg));//向标准输出即终端写入
close(save_fd); //关闭save_fd标识的tty
return 0;
}
/*
*父进程调用fork,进入内核,此时复制出一个子进程,内容和父进程一样,执行和父进程相同的程序,此后两进程相互独立
*此时两个一模一样的进程看似都调用了fork等待从内核返回
*事实上fork只调用了一次,只是要在父子进程各返回一次,谁先返回?取决于内核的进程调度
*每个时刻只有一个进程被执行,使用sleep,让此进程睡眠一会,使得其他进程有机会被调度执行
*/
#include
#include
#include
#include
int main(void) {
pid_t pid;
char *message;
int n;
pid = fork();
if(pid < 0) {
perror("fork failed");
exit(1);
}
if(pid == 0) {
message = "This is the child\n";
n = 6;
}
else {
message = "This is the parent\n";
n = 3;
}
for(; n > 0; n--) {
printf(message);
sleep(1);
}
return 0;
}
///////////////////////
$ ./a.out
This is the child
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
$ This is the child
This is the child
/////////////////////////////
1. 父进程初始化。
2. 父进程调用fork,这是一个系统调用,因此进入内核。
3. 内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态代码和数据也相同。因此,子进程现在的状态看起来和父进程一样,做完了初始化,刚调用了fork进入内核,还没有从内核返回。
4. 现在有两个一模一样的进程看起来都调用了fork进入内核等待从内核返回(实际上fork只调用了一次),此外系统中还有很多别的进程也等待从内核返回。是父进程先返回还是子进程先返回,还是这两个进程都等待,先去调度执行别的进程,这都不一定,取决于内核的调度算法。
5. 如果某个时刻父进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是子进程的id,是一个大于0的整数,因此执下面的else分支,然后执行for循环,打印"This is the parent\n"三次之后终止。
6. 如果某个时刻子进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是0,因此执行下面的if (pid == 0)分支,然后执行for循环,打印"This isthe child\n"六次之后终止。fork调用把父进程的数据复制一份给子进程,但此后二者互不影响,在这个例子中,fork调用之后父进程和子进程的变量message和n被赋予不同的值,互不影响。
7. 父进程每打印一条消息就睡眠1秒,这时内核调度别的进程执行,在1秒这么长的间隙里(对于计算机来说1秒很长了)子进程很有可能被调度到。同样地,子进程每打印一条消息就睡眠1秒,在这1秒期间父进程也很有可能被调度到。所以程序运行的结果基本上是父子交替出现
8. 这个程序是在Shell下运行的,因此Shell进程是父进程的父进程。父进程运行时Shell进程处于等待状态(第 3.3 节 “wait和waitpid函数”会讲到这种等待是怎么实现的),当父进程终止时Shell进程认为命令执行结束了,于是打印Shell提示符,而事实上子进程这时还没结束,所以子进程的消息打印到了Shell提示符后面。最后光标停在This is the child的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,Shell也能正确读取。
fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。子进程仍可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法。
fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加
/*
*僵尸进程:子进程终止了,父进程不对其清理(什么也不做譬如while在那里);
*若父进程终止了,则init进程将清理僵尸进程
*/
#include
#include
int main(void) {
pid_t pid = fork();
if(pid < 0) {
perror("fork");
exit(1);
}
if(pid > 0) { //父进程流程
//parent
while(1); //父进程不调用wait/waitpid收子进程的尸体,此时子进程变为僵尸进程
}
//child
return 0; //子进程流程,子进程非正常死亡(return, exit)
}/*
*waitpid查看子进程退出时的状态,即父进程调用waitpid时将阻塞,直到子进程退出,从而得到子进程的退出状态
*WIFEXITED正常退出,打印子进程退出状态
*WIFSIGNALED收到异常信号而终止,打印信号的编号
**在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等.
*但是仍然为其保留一定的信息(包括进程号,退出状态,运行时间等), 直到父进程通过wait / waitpid来取时才释放.
*如果父进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,
*其进程号就会一定被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程
*/
#include
#include
#include
#include
#include
int main(void) {
pid_t pid;
pid = fork();
if(pid < 0) {
perror("fork failed");
exit(1);
}
if(pid == 0) { //子进程流程
int i;
for(i = 3; i > 0; i--) {
printf("This is the child\n");
sleep(1);
}
exit(3); //子进程非自然死亡,发送一个SIG_CHILD信号给父进程
//perror("child abort");
}
else { //父进程流程
int stat_val;
waitpid(pid, &stat_val, 0); //为子进程收尸,收到SIG_CHILD这个信号,进行处理;否则子进程将变为僵尸进程
if(WIFEXITED(stat_val))
printf("Child exited with code %d\n",WEXITSTATUS(stat_val));
else if(WIFSIGNALED(stat_val))
printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
}
return 0;
}
当一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止 时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
如果一个进程已经终止(非正常死亡,即没有执行到”}”),但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程
在./a.out命令后面加个&表示后台运行
#ps -aux |grep zombie //查看进程详细状态
如果一个父进程终止,而它的子进程还存在(这些子进程或者仍在运行,或者已经是僵尸进程了),则这些子进程的父进程改为init进程。init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止,init就会调用wait函数清理它。
如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。
wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。
可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。
进程也一样,它可以是自然死亡,即运行到main函数的最后一个"}",从容地离我们而去;也可以是自杀,自杀有2种方式,一种是调用exit函数, 一种是在main函数内使用return,无论哪一种方式,它都可以留下遗书,放在返回值里保留下来;它还甚至能可被谋杀,被其它进程通过另外一些方式结束他的生命(这里跟人有些不一样,在进程里,如果父进程死了,那么他创建的所有子进程也一起跟着死去)
/*
*管道:fd[0]读端;fd[1]写端
*父进程创建pipe后,然后fork,此时子进程拷贝了同一份父进程的pipe,两pipe指向同一个内核缓冲区
*父进程从写端写入数据,并且关闭读端;子进程从读端读取数据,并且关闭写端;
*从写端写入,读端读出,从而达到进程间数据共享
*/
#include
#include
#define MAXLINE 80
int main(void) {
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if(pipe(fd) < 0) { //建立管道
perror("pipe");
exit(1);
}
if((pid = fork()) < 0) { //创建子进程
perror("fork");
exit(1);
}
if(pid > 0) {
close(fd[0]); //父进程关闭读端
write(fd[1], "hello world\n", 12); //从写端写入
wait(NULL); //阻塞等待子进程读管道/收尸
}else {
close(fd[1]); //子进程关闭写端
n = read(fd[0], line, MAXLINE); //从读端读取数据
write(STDOUT_FILENO , line, n); //终端显示读取的数据
}
return 0;
}/*
*1S之后终止当前进程
*/
#include
#include
int main(void) {
int counter;
alarm(1);
for(counter=0; 1; counter++)
printf("counter=%d\n", counter);
return 0;
}
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
Ctrl-\产生SIGQUIT信号,
Ctrl-Z产生SIGTSTP信号
Ctrl-C,这个键盘输入产生一个硬件中断。
Ctrl-C产生的信号只能发给前台进程。
Shell可以同时运行一个前台进程和任意多个后台进程
用kill -l命令可以察看系统定义的信号列表:
/*
*一个信号有两个标志:阻塞和未决,用sigset_t类型的一个位来表示,1表示阻塞/未决,0相反
*流程:1定义信号集变量,2然后初始化为有效或无效状态,3设置要阻塞的信号
*操作:该程序阻塞了SIGINT信号,即屏蔽了Ctrl+c信号,但Ctrl+\则可以递达
*/
#include
#include
void printsigset(const sigset_t *set) {
int i;
for(i = 1; i < 32; i++)
if(sigismember(set, i) == 1) //判断set信号集中的有效信号是否包含某种信号
putchar('1');
else
putchar('0');
puts("");
}
int main(void) {
sigset_t s,p; //定义信号集变量
sigemptyset(&s); //初始化S指向的信号集,使其处于确定状态,即不包含有效信号
sigaddset(&s, SIGINT); //向S信号集中加入SIGINT信号
sigprocmask(SIG_BLOCK, &s, NULL);//设置S信号集的屏蔽字,即加入了对SIGINT信号的阻塞
while(1) {
sigpending(&p); //读取当前进程的未决信号集,通过p参数传出
printsigset(&p); //打印未决信号集的状态
sleep(1); //每过1S打印一次
}
return 0;
}
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志
Linux是这样实现的:
常规信号在递达之前产生多次只计一次,
而实时信号在递达之前产生多次可以依次放在一个队列里。
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态
#include
int sigemptyset(sigset_t *set);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,
表示该信号集不包含任何有效信号
在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化使信号集处于确定的状态。
int sigfillset(sigset_t *set);
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,
表示该信号集的有效信号包括系统支持的所有信号
int sigaddset(sigset_t *set, int signo);
初始化sigset_t变量之后就可以在调用sigaddset在该信号集中添加某种有效信号。
int sigdelset(sigset_t *set, int signo);
初始化sigset_t变量之后就可以在调用sigdelset在该信号集中删除某种有效信号
这四个函数都是成功返回0,出错返回-1。
int sigismember(const sigset_t *set, int signo);
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,
若包含则返回1,不包含则返回0,出错返回-1。
调用函数sigprocmask可以读取或更改进程的信号屏蔽字
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
int sigpending(sigset_t *set);
sigpending读取当前进程的未决信号集,通过set参数传出
/*
*pause//挂起当前进程,直到有信号
*使用闹钟alarm长生SIGALRM信号
*sigaction//为SIGALRM信号指定处理动作,不用默认
*/
#include
#include
#include
void sig_alrm(int signo) {
//nothing to do //自定义的SIGALRM信号的处理函数,位于用户空间
}
unsigned int mysleep(unsigned int nsecs) {
struct sigaction newact, oldact;
unsigned int unslept;
newact.sa_handler = sig_alrm; //关联SIGALRM信号的处理函数
sigemptyset(&newact.sa_mask); //初始化信号集
newact.sa_flags = 0; //字段的某些选项,这里不考虑
sigaction(SIGALRM, &newact, &oldact);//为SIGALRM指定处理动作(newact),同时将原来的处理动作通过oldact传出
alarm(nsecs); //设置nsecs秒的闹钟
pause(); //挂起当前进程,直到有信号,这里为SIGALRM信号,即挂起2S
unslept = alarm(0); //取消闹钟
sigaction(SIGALRM, &oldact, NULL);//为SIGALRM指定处理动作(oldact),即还原SIGALRM信号的默认处理动作
return unslept;
}
int main(void) {
while(1) {
mysleep(2); //睡眠2S
printf("Two seconds passed\n");//每睡眠2S打印一次
}
return 0;
}
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
signo是指定信号的编号。
若act指针非空,则根据act修改该信号的处理动作。
若oact指针非空,则通过oact传出该信号原来的处理动作。
act和oact指向sigaction结构体:
struct sigaction {
void (*sa_handler)(int); /* 自定义信号处理函数, */
/* or SIG_IGN, or SIG_DFL */
sigset_t sa_mask; /* 处理一个信号的时候屏蔽另一个信号*/
int sa_flags; //不关心就赋值0
void (*sa_sigaction)(int, siginfo_t *, void *); //实时信号处理函数,不关心就不管
};
int pause(void);
pause函数使调用进程挂起直到有信号递达。
如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;
如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;
如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR
错误码EINTR表示“被信号中断”。
/*
*守护进程(daemon):系统服务进程,不受用户登录注销的影响,一直运行
*ps axj :命令可以查看所有用户的守护进程; ps xj | gerep xxx(守护进程的名字)
*运行该程序,它就变成了一个守护进程,不再和当前终端关联,使用ps axj命令查看
*即使重新打开终端或注销也不影响该守护进程,使用kill xxx(pid)可结束
*/
#include
#include
#include
void daemonize(void) {
pid_t pid;
if((pid = fork()) < 0) {
perror("fork");
exit(1);
} else if(pid != 0) {
//父进程什么也不做,直接退出
exit(0);
}
setsid(); //必须由子进程来调用该函数,创建Session,成为Session Leader
if(chdir("/") < 0) { //创建守护进程后,通常将当前工作目录切换到根目录
perror("chdir");
exit(1);
}
close(0); //由于当前进程有一个控制终端,现在该进程则失去该控制终端
open("/dev/null", O_RDWR);
dup2(0, 1); //创建守护进程后,通常将文件描述符0,1,2重定向到/dev/null
dup2(0, 2);
}
int main(void) {
daemonize();
while(1);
}
/*
*线程之间函数,全局变量共享,各线程之间受CPU的调度轮询
*只要有任何一个线程调用了exit或_exit,则所有的线程就结束
*主线程return返回之后,所有的线程都结束
*编译时要加上-lpthread
*/
#include
#include
#include
#include
#include
pthread_t ntid; //定义一个全局的线程号,使得其他线程也可以访问到
void printids(const char *s) {
pid_t pid;
pthread_t tid;
pid = getpid(); //打印当前进程id
tid = pthread_self(); //打印当前线程id
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
}
void *thr_fn(void *arg) { //新线程要执行的代码
printids(arg); //主线程和新线程之间也受CPU的调度轮询
return NULL;
}
int main(void) {
int err;
err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
//参数:新创建线程id,属性,所要执行的代码,传递给所要执行的代码的参数;失败返回错误码
if(err != 0) {//由于pthread_creat返回的错误码不保存在errno中,不能perror("错误码")
fprintf(stderr, "can't creat thread: %s\n", strerror(err));//故使用strerror将错误码转换成错误信息再打印
exit(1);
}
printids("main thread:");
sleep(1);//只要有任何一个线程调用了exit或_exit,则所有的线程就结束,为了使新线程有机会运行,故睡眠一下
return 0;//主线程return返回之后,所有的线程都结束
}
进程在各自独立的地址空间中运行,进程之间共享数据需要用mmap或者进程间通信机制,
有些情况需要在一个进程中同时执行多个控制流程,这时候线程就派上了用场,
比如实现一个图形界面的下载软件,一方面需要和用户交互,等待和处理用户的鼠标键盘事件,
另一方面又需要同时下载多个文件,等
操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。
由于同一进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,
如果定义一个函数,在各线程中都可以调用,
如果定义一个全局变量,在各线程中都可以访问到
各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
但有些资源是每个线程各有一份的:
线程id
上下文,包括各种寄存器的值、程序计数器和栈指针
栈空间
errno变量
信号屏蔽字
调度优先级
在Linux上线程函数位于libpthread共享库中,因此在编译时要加上-lpthread选项。
#include
int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void*),
void *restrict arg);
返回值:成功返回0,失败返回错误号
在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,
而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。
start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,
该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。
start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。
start_routine返回时,这个线程就退出了,
其它线程可以调用pthread_join得到start_routine的返回值,
新创建的线程的id被填写到thread参数所指向的内存单元。
我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,
线程id的类型是thread_t,它只在当前进程中保证是唯一的,
/*
*pthread_join调用后挂起,直到指定的线程终止,获取其终止的状态
*对于终止的线程,其终止状态不能用一个该线程内的局部变量保存;否则,pthread_join无法取得其终止状态
*终止各自的线程有三种方法:
*1,线程内部return
*2,线程内部调用pthread_exit终止自己,终止状态是pthread_exit的参数
*3,其他线程(这里为主线程)调用pthread_cancel终止同一进程中的另一个线程,终止状态为一个常数
*编译时需加-lpthread
*对同一个线程不能两次pthread_join,否则该线程变为detach状态,即该线程终止时不保存终止状态;pthread_detach可设置线程为detach状态
*/
#include
#include
#include
#include
void *thr_fn1(void *arg) { //线程1执行的代码
printf("thread 1 returning\n");
return (void *)1; //自我返回,线程1结束,保留返回码,直到pthread_join获取到它的结束状态才释放//若返回码存于一个变量,则该变量需为全局或malloc分配
}
void *thr_fn2(void *arg) { //线程2执行的代码
printf("thread 2 exiting\n");
pthread_exit((void *)2); //自我终止,线程2结束,保留终止码为一个常数,直到pthread_join获取到它的结束状态才释放
}
void *thr_fn3(void *arg) { //线程3执行的代码
while(1) {
printf("thread 3 writing\n");
sleep(1);
}
}
int main(void) {
pthread_t tid; //各线程pthread_create后拥有自己的tid
void *tret; //指向各线程退出的状态
pthread_create(&tid, NULL, thr_fn1, NULL);//线程1创建,还没运行新线程1的代码,往下 pthread_join(tid, &tret); //主线程挂起,直到指定的线程终止,即线程1,获取其终止的状态
printf("thread 1 exit code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn2, NULL);//线程2创建,还没运行新线程2的代码 pthread_join(tid, &tret); //主线程挂起,直到线程2终止,获取其终止的状态
printf("thread 2 eixt code %d\n", (int)tret);
pthread_create(&tid, NULL, thr_fn3, NULL);//线程3创建,还没运行新线程3的代码 sleep(3); //让线程3有机会运行,这里运行3次
pthread_cancel(tid); //主线程结束指定的线程,即线程3
pthread_join(tid, &tret); //获取线程3的终止状态
printf("thread 3 exit code %d\n", (int)tret);
return 0; //主线程结束,其他子线程也随之而结束
}
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
3线程可以调用pthread_exit终止自己。
void pthread_exit(void *value_ptr);
其它线程可以调用pthread_join获得这个指针
pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
int pthread_join(pthread_t thread, void **value_ptr);
调用该函数的线程将挂起等待,直到id为thread的线程终止。
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
2如果thread线程被别的线程调用pthread_cancel异常终止掉,value_ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
3如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
/*
*多线程同时(非【读-修改-写】一个原子操作)访问共享数据时可能会冲突
*每个线程各自将counter+5000次,但每次运行的结果可能都不一样
*当调用printf函数时,会执行write系统调用进入内核,为内核调度别的线程提供了时机
*因而出现线程1在运行时printf函数被内核调度打断,线程2读共享数据的时候printf函数又被内核调度打断,回到线程1,此时线程2并没有完成一个原子操作,仅仅读取了共享数据,如此反复,故执行的结果不一样
*/
#include
#include
#include
#define NLOOP 5000
int counter; //全局变量,线程之间共享
void *doit(void *);
int main(int argc, char **argv) {
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, &doit, NULL);
pthread_create(&tidB, NULL, &doit, NULL);
pthread_join(tidA, NULL); //表明上流程阻塞在这里,直到线程A结束,但实际上线程B可能此时也得以执行,因为线程之间受内核的调度执行,也可能线程B先结束了,不过一定要等线程A结束了,才能往下执行流程
pthread_join(tidB, NULL); //确保线程A,B都被执行完了,即总的完成了10000次
return 0;
}
void *doit(void *vptr) {
int i, val;
for(i = 0; i < NLOOP; i++) {
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val+1); //该语句会执行write系统调用,进入内核,线程间便调度,为线程B的运行提供了时机,此时就可能出现共享数据的冲突,各线程没有独立的完成【读-修改-写】这个原子操作
counter = val + 1;
}
return NULL;
}
/*
*多线程同时(非【读-修改-写】一个原子操作)访问共享数据时可能会冲突
*解决办法是引入互斥锁(mutex)
*互斥锁保证每个线程都完成一个原子操作【读-修改-写】
*从而解决了多线程访问共享数据时的冲突
*实现了线程内的挂起
*互斥锁变量的值非0即1;可看作一种资源的可用数量,初始化时=1:表示有一个可用资源
*加锁获得该资源,将互斥锁变量减到0:表示不再有可用资源,会被挂起
*解锁时释放该资源,将互斥锁变量重新加到1,表示又有了一个可用资源
*/
#include
#include
#include
#define NLOOP 5000
int counter; //全局变量,线程之间共享
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; //定义了一个互斥锁变量counter_mutex,即将全局变量这个共享数据封装为互斥锁变量,并给锁初始化
void *doit(void *);
int main(int argc, char **argv) {
pthread_t tidA, tidB;
pthread_create(&tidA, NULL, &doit, NULL);
pthread_create(&tidB, NULL, &doit, NULL);
pthread_join(tidA, NULL); //表明上流程阻塞在这里,直到线程A结束,但实际上线程B可能此时也得以执行,因为线程之间受内核的调度执行,也可能线程B先结束了,不过一定要等线程A结束了,才能往下执行流程
pthread_join(tidB, NULL); //确保线程A,B都被执行完了
return 0;
}
void *doit(void *vptr) {
int i, val;
for(i = 0; i < NLOOP; i++) {
pthread_mutex_lock(&counter_mutex); //这个互斥锁变量获得了锁,此时另一个线程访问的话,将会被挂起,资源-1=0
//以下代码都被锁在里面
val = counter;
printf("%x: %d\n", (unsigned int)pthread_self(), val+1); //该语句会执行write系统调用,进入内核,线程间便调度,为线程B的运行提供了时机,但线程B访问该段被锁在里面的代码后,会被挂起,此时就解决了共享数据的冲突,各线程能够独立的完成【读-修改-写】这个原子操作
counter = val + 1;
//以上代码都被锁在里面
pthread_mutex_unlock(&counter_mutex); //这个互斥锁变量释放了锁,此时另一个线程才可以访问资源+1=1
}
return NULL;
}
对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,MutualExclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
Mutex的加锁和解锁操作可以用下列函数:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
一个线程可以调用pthread_mutex_lock获得Mutex,
如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,
直到另一个线程调用pthread_mutex_unlock释放Mutex,
当前线程被唤醒,才能获得该Mutex并继续执行
每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。
如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。另一种典型的死锁情形是这样:线程A获得了锁1,线程B获得了锁2,
/*防止了死锁的产生
*定义条件锁变量,实现线程内部挂起,相当于锁上上锁,通常和互斥锁联用
*pthread_cond_wait线程内部挂起直到条件锁变量满足
*pthread_cond_signal唤醒那个等待的线程,使条件锁变量得到满足
*/
#include
#include
#include
struct msg {
struct msg *next;
int num;
};
struct msg *head; //共享数据区
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; //定义了一个条件锁变量
pthread_mutex_t lock = PTHREAD_MUTEX_INTTIALIZER; //定义互斥锁变量
void *consumer(void *p) {
struct msg *mp;
for(;;) {
pthread_mutex_lock(&lock); //上锁
while(head == NULL)
pthread_cond_wait(&has_product, &lock); //先释放锁,线程内部挂起等待某个条件变量满足,然后再获得锁,相当于锁上上锁
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock); //解锁
printf("Consume %d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p) {
struct msg *mp;
for(;;) {
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;
printf("Produce %d\n", mp->num);
pthread_mutex_lock(&lock); //上锁
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock); //解锁
pthread_cond_signal(&has_product); //唤醒那个等待条件变量的线程
sleep(rand() % 5);
}
}
int main(int argc, char *argv[]) {
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, &producer, NULL);
pthread_create(&cid, NULL, &consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或者唤醒等待这个条件的线程。
Condition Variable用pthread_cond_t类型的变量表示,可以这样初始化和销毁:
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
Condition Variable的操作可以用下列函数
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);//防止了死锁
pthread_cond_wait在一个Condition Variable上阻塞等待,这个函数做以下三步操作:
1. 释放Mutex,防止了死锁
2. 阻塞等待
3. 当被唤醒时,重新获得Mutex并返回
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒这个cond上的所有线程
int pthread_cond_signal(pthread_cond_t *cond); //唤醒符合这个cond的一个线程
/*
*信号量semaphore,表示可用资源的数量,数量可以>1
*实现线程内部的挂起
*可用于同一进程的线程间同步,也可用于不同进程间的同步
*信号变量的值>0表示有可用资源,=0表示没有资源,会被挂起
*/
#include
#include
#include
#define NUM 5
int queue[NUM];
sem_t blank_number, product_number; //定义信号变量
void *producer(void *arg) {
int p = 0;
while(1) {
sem_wait(&blank_number); //若信号变量的值!=0,则可以获得资源,将信号变量的值-1;相反则挂起
queue[p] = rand() % 1000 + 1;
printf("Produce %d\n", queue[p]);
sem_post(&product_number); //释放资源,使信号变量的值+1,同时唤醒挂起的线程
p = ( p+1 ) % NUM;
sleep(rand() % 5);
}
}
void *consumer(void *arg) {
int c = 0;
while(1) {
sem_wait(&product_number); //若信号变量的值=0,表示没有资源,将当前线程挂起;相反则获得资源,将信号变量值-1
printf("Consume %d\n", queue[c]);
queue[c] = 0;
sem_post(&blank_number); //释放资源,使信号变量的值+1,同时唤醒挂起的线程
c = (c + 1) % NUM;
sleep(rand() % 5);
}
}
int main(int argc, char *argv[]) {
pthread_t pid, cid;
sem_init(&blank_number, 0, NUM);//初始化信号变量,作用于同一进程间的多线程,可用资源数=5
sem_init(&product_number, 0, 0);//初始化信号变量,作用于同一进程间的多线程,可用资源数=0
pthread_create(&pid, NULL, &producer, NULL);
pthread_create(&cid, NULL, &consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
sem_destroy(&blank_number); //销毁信号变量
sem_destroy(&product_number);
return 0;
}
信号量(Semaphore)和Mutex类似,表示可用资源的数量,和Mutex不同的是这个数量可以大于1。
这种信号量不仅可用于同一进程的线程间同步,也可用于不同进程间的同步。
int sem_init(sem_t *sem, int pshared, unsigned int value);
semaphore变量的类型为sem_t,
sem_init()初始化一个semaphore变量,value参数表示可用资源的数量,
pshared参数为0表示信号量用于同一进程的线程间同步
int sem_wait(sem_t *sem);
调用sem_wait()可以获得资源,使semaphore的值减1,
如果调用sem_wait()时semaphore的值已经是0,则挂起等待。
int sem_trywait(sem_t *sem);
如果不希望挂起等待,可以调用sem_trywait()。
int sem_post(sem_t * sem);
调用sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程。
int sem_destroy(sem_t * sem);
/* *作为服务端,从客户端读取字符,转换为大写返回给客户端 *编译后作为服务端先运行,重新打开一个终端,查看状态:netstat
-apn|grep 8000 *重新打开一个终端,编译客户端,运行:./client
abcd *将看到被服务端转换为大写的字符 *同时查看运行服务端的终端,可以看到服务端打印出客户端的地址和端口 *监听文件描述符监听客户端的连接;通讯文件描述符用于通讯 *通讯都是借助socket作为通讯的桥梁 */ #include #include #include #include #include #include #define MAXLINE 80 #define SERV_PORT 8000 int main(void) { struct sockaddr_in servaddr,
cliaddr; //定义IPV4结构体的变量servaddr,cliaddr socklen_t cliaddr_len; //客户端结构体的长度 int listenfd, connfd; //监听文件描述符和链接文件描述符 char buf[MAXLINE]; //网络数据收发缓冲区 char str[INET_ADDRSTRLEN]; int i, n; listenfd = socket(AF_INET,
SOCK_STREAM, 0); //打开一个网络通信端口,成功的话,返回一个文件描述符,应用程序通过该文件描述符读写网络收发数据;IPV4第一个参数为AF_INET,使用TCP协议,第二个参数为SOCK_STREAM,第三个参数略 //对服务端结构体内的参数进行初始化 bzero(&servaddr,
sizeof(servaddr)); //将服务端结构体清零 servaddr.sin_family = AF_INET; //设置地址类型为AF_INET,因为使用IPV4 servaddr.sin_addr.s_addr =
htonl(INADDR_ANY);//网络地址为INADDR_ANY,对本机任意IP地址进行监听,因为可能本机有多个网卡,这样的话就可以监听到多个网卡的IP servaddr.sin_port =
htons(SERV_PORT); //服务端端口号为SERV_PORT,定义为8000 bind(listenfd, (struct sockaddr
*)&servaddr, sizeof(servaddr)); //将监听文件描述符和服务端绑定在一起,使网络通信的文件描述符监听服务端所描述的地址和端口号;客户端事先知道服务端的地址和端口,进行连接;绑定是为了避免每次重启服务端的时候,内核自动重新给服务端分配端口号,使得客户端连接遇到麻烦 listen(listenfd, 20); //当有客户端发起连接时(多个),listen()申明这个监听文件描述符处于监听状态,并且只允许最多20个客户端的请求处于等待连接状态,多余的请求将忽略 printf("Accepting
connections ...\n"); while(1) { //服务端死循环进行监听,连接,通信的主体;每次循环处理一个客户端的连接 cliaddr_len = sizeof(cliaddr); //得到客户端结构体的长度 connfd = accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len); //进过三方握手后,服务端调用accept()接受连接;返回一个连接状态的文件描述符,接下来的通讯就使用该文件描述符;第一个参数为监听文件描述符,第二个参数为客户端的结构体,传出客户端的地址和端口号,第三个参数传入客户端缓冲区长度,传出客户端地址结构体的实际长度 n = read(connfd, buf,
MAXLINE); //使用连接文件描述符进行通信;通过网络数据收发缓冲区读取客户端发来的数据 printf("received from %s
at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str,
sizeof(str)), ntohs(cliaddr.sin_port)); //打印客户端的地址和通讯端口号 for(i = 0; i < n; i++) //循环处理收到的长度为n的数据 buf[i] = toupper(buf[i]); //在网络数据收发缓冲区进行大写转换 write(connfd, buf, n); //使用通信的连接文件描述符将转换后的字符写到缓冲区中,即发送数据 close(connfd); //一次通讯完毕,关闭通讯用的连接文件描述符,处理下一次连接 } } 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出, 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存, 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。 TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。 例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8), 则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8, 这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。 但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。 因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。 同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。 如果主机是大端字节序的,发送和接收都不需要做转换。 同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。 为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行, 可以调用以下库函数做网络字节序和主机字节序的转换。 #include
uint32_t
htonl(uint32_t hostlong); uint16_t
htons(uint16_t hostshort); uint32_t
ntohl(uint32_t netlong); uint16_t
ntohs(uint16_t netshort); socket API是一层抽象的网络编程接口,适用于各种底层网络协议, /* *作为客户端,将命令行参数中的一个字符串发给服务端,然后接收服务端返回的字符串并打印 *让服务端先运行,重新打开一个终端,查看状态:netstat
-apn|grep 8000 *重新打开一个终端,编译客户端,运行:./client abcd *将看到被服务端转换为大写的字符 *同时查看运行服务端的终端,可以看到服务端打印出客户端的地址和端口 *监听文件描述符在客户端上用于通讯 *通讯都是借助socket作为通讯的桥梁 */ #include #include #include #include #include #include #define MAXLINE 80 #define SERV_PORT 8000 //定义服务端端口,使用该端口通讯 int main(int argc, char *argv[]) { struct
sockaddr_in servaddr; //定义IPV4服务端地址结构体的变量servaddr char
buf[MAXLINE]; //定义网络数据收发缓冲区 int
sockfd, n; //定义监听文件描述符 char
*str; if(argc
!= 2) { //提示输入命令行参数 fputs("usage:
./client message\n", stderr); exit(1); } str
= argv[1]; //取出命令行参数的第一个参数 sockfd
= socket(AF_INET, SOCK_STREAM, 0); //打开一个网络通信端口,成功的话,返回一个文件描述符,应用程序通过该文件描述符读写网络收发数据;IPV4第一个参数为AF_INET,使用TCP协议,第二个参数为SOCK_STREAM,第三个参数略 //对服务端结构体内的参数进行初始化 bzero(&servaddr,
sizeof(servaddr)); //将服务端结构体清零 servaddr.sin_family
= AF_INET; //设置地址类型为AF_INET,因为使用IPV4 inet_pton(AF_INET,
"127.0.0.1", &servaddr.sin_addr); //作为演示,由于服务端和客户端位于同一主机上,所以使用回环测试IP;否则填写服务端主机IP servaddr.sin_port
= htons(SERV_PORT); //服务端端口号为SERV_PORT,定义为8000 connect(sockfd,
(struct sockaddr *)&servaddr, sizeof(servaddr)); //通过这个监听文件描述符主动连接服务端 write(sockfd,
str, strlen(str)); //往监听文件描述符的文件写入要发送给服务端的数据,即给服务端发数据 n
= read(sockfd, buf, MAXLINE); //往监听文件描述符的文件读取从服务端发来的数据 printf("Response
from server:\n"); write(STDOUT_FILENO,
buf, n); //将收到的数据打印在屏幕上 close(sockfd); //一次通讯完毕,关闭监听文件描述符 return
0;