今天继续来学习
exit函数
这个函数是用来终止程序的继续执行
系统调用格式:
int status;
void exit(int status);
其中,status是返回给父进程的一个整数,以备查考。
为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit()来实现进程的自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit(),使子进程自我终止。exit(0)表示进程正常终止,exit(1)表示进程运行有错,异常终止。
如果调用进程在执行exit()时,其父进程正在等待它的终止,则父进程可立即得到其返回的整数。内核须为exit()完成以下操作:
(1)关闭软中断
(2)回收资源
(3)写记账信息
(4)置进程为“僵死状态”
接下来我们来看一组代码:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
pid_t pid = fork();
int status;
if(pid == 0)
{
printf("1-1 i am child pid = %d,my parent pid = %d\n",getpid(),getppid());
sleep(10);
printf("---------1-1 child going to die--------\n");
//exit(0);
printf("1-2 i am child pid = %d,my parent pid = %d\n",getpid(),getppid());
sleep(10);
printf("--------1-2 child going to die----------\n");
}
else if(pid>0)
{
while(1)
{
pid-t wpid = wait(status);
if(wpid<0)
{
perror("wpid");
exit(1);
}
printf("wait wpid = %d\n",wpid);
printf("i am parent,my pid id = %d\n",getpid());
sleep(1);
}
}
else
{
perror("fork");
return 1;
}
return 0;
}
让我们看看这串代码,首先我们用pid来fork了一个进程然后再它的子进程中执行俩步操作,在其父进程里我们用一个wait函数来让这个父进程来一直等这个子进程结束后再执行父进程里的东西。我们可以看到wpid其实就是这个子进程的id。
那么exit函数的参数到底有什么意义?
C 语言的设计之初就是为 Unix 系统设计的,而这个系统是『很多程序互相配合』搭配成一个系统。
每个运行着的程序都是进程,而进程就会有父进程,父进程通常是直接启动你的进程,父进程死亡的进程会被 init 收养,其父进程变为 init,而 init 的父进程是进程 0,进程 0 则是系统启动时启动的第一个进程。
exit() 里面的参数,是传递给其父进程的。对父进程来说,你的进程仿佛是一个函数,而函数可以有返回值。
所以回答第一个问题:exit() 的参数,是给自己的父进程使用的。通常一个程序的父进程可能是任何进程,因此我们无法预期我们的父进程是否规定必须要有这个返回值,那么我们应当提供这个返回值,以保证不同的父进程的需求得到满足。
一个典型的例子是 make,Makefile 对于一个 target 下面有多条顺序执行的语句,而 make 作为父进程,会检查每个语句的返回值是否为 0 ,遇到任何一个非 0 值,都会停止当前 rule 的执行。而我们知道,make 实际上可以执行任何命令任何程序,因而任何被 make 调用的程序必须有正确的返回值。
另外一个问题,为什么要使用 exit() 函数?
答:是历史原因,虽然现在大多数平台下,直接在 main() 函数里面 return 可以退出程序。但是在某些平台下,在 main() 函数里面 return 会导致程序永远不退出(因为代码已经执行完毕,程序却还没有收到要退出的指令)。换句话说,为了兼容性考虑,在特定的平台下,程序最后一行必须使用 exit() 才能正常退出,这是 exit() 存在的重要价值。
注:kill发信号结束进程时,谁创建谁kill(比如用户)。
常见的进程间的通信方法
ipc方法
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC InterProcess Communication)。
在进程间完成数据传递需要借助操作系统提供的特殊方法,比如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现如今常用的进程间通信方式有:
1.管道(使用最简单)。
2.信号(开销最小)。
3.共享映射区(无血缘关系)。
4.本地套接字(最稳定)。
pipe管道
管道是Linux中进程间通信的一种方式,作用于有血缘关系的进程之间,完成数据传递。
管道允许在进程之间按先进先出的方式传送数据,是进程间通信的一种常见方式。
管道是Linux 支持的最初Unix IPC形式之一,具有以下特点:
(1) 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
(2) 匿名管道只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
(3) 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
管道分为pipe(无名管道)和fifo(命名管道)两种,除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。
- pipe用于相关进程之间的通信,例如父进程和子进程,它通过pipe()系统调用来创建并打开,当最后一个使用它的进程关闭对他的引用时,pipe将自动撤销。
- FIFO即命名管道,在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,通过mknode()系统调用或者mkfifo()函数来建立的。一旦建立,任何进程都可以通过文件名将其打开和进行读写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。
管道的局限性:
1.数据不能进程自己写,自己读。
2.管道中的数据不可反复读取。一旦读走,管道中就不复存在。
3.采用半双工的通信方式,数据只能在单方向上流动。
4.只能在有公共祖先的进程间使用管道
常见的通信方式有:单工通信、半双工通信、全双工通信。
pipe函数
创建管道
int pipe(int fd【2】);
//成功返回0;fd【0】是读端,fd【1】是写端
//失败返回-1;设置errno
函数调用成功返回r/w这俩个文件描述符。无需open,但要手动close。规定fd【0】为读端(r);fd【1】为写端(w),就像是0对应标准输入,1对应标准输出一样。向管道文件读写数据其实就是在读写内核缓冲区。
管道创建成功后,创建该管道的进程(父进程)同时掌握着管道得到读端和写端。通常采用如下步骤实现父子进程间的通信:
1.父进程调用pipe函数创建管道,得到俩个文件描述符fd【0】、fd【1】指向管道的读端和写端。
2.父进程调用fork创建子进程,那么子进程也有俩个文件描述符指向同一管道。
3.父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。
由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间的通信。
父子通信
例程:父子进程使用管道通信,父写入字符串,子进程读出并打印到屏幕上。
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
void syd_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int ret;
int fd[2];
pid_t pid;
char *str = "hello pipe\n";
char buf[1024] = "\0";
ret = pipe(fd);
if(ret<0)
{
sys_err("pipe error");
}
pid = fork();
if(pid>0)
{
close(fd[0]);
write(fd[1],str,strlen(str));
close(fd[1]);
}
else if(fd == 0)
{
close(fd[1]);
ret = read(fd[0],buf,sizeof(buf));
write(STDOUT_FILENO,buf,ret);
close(fd[0]);
}
else
{
sys_err("fork error");
}
return 0;
}
兄弟进程通信
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/types.h>
#define BUFSIZE 1024
void err_quit(char *str)
{
perror(str);
exit(1);
}
int main()
{
int fd[2];
char buf[BUFSIZE];
pid_t pid;
int len;
if((pipe(fd)<0))//创建管道
{
err_quit("pipe");
}
if((pid = fork())<0)//创建进程
{
err_quit("fork");
}
else if(pid == 0)//子1进程
{
close(fd[0])//关闭读端
write(fd[1],"hello brether!",14);//子1进程写入信息
exit(0);
}
else
{
if((pid = fork())<0)//常见子2进程
{
err_quit("fork");
}
else if(pid == 0)
{
close(fd[1]);//关闭写端
len = read(fd[0],buf,BUFSIZE);//子2进程读取管道内容
write(STDOUT_FILENO,buf,len);//子2进程把读到的内容写到显示端
printf("\n");
exit(0);
}
esle
{//父进程关闭无用端口
close(fd[0]);
close(fd[1]);
sleep(1);
exit(0);
}
}
return 0;
}
管道读写行为
一.读管道
1.管道中有数据
(1)read返回实际读到的字节数,一旦数据读走,就没有数据了。
(2)可以允许多个读端,但存在竞争关系,速度快得进程读走后,数据就没有了。(通过时间控制,可以确定哪个进程先读走)
2. 管道中无数据
(1)管道写端被全部关闭,read返回0(好像读到文件末尾)。
(2)写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)。
二.写管道
1.管道读端全部被关闭,进程异常终止;
2.管道读端没有全部关闭;
(1)管道已满,write阻塞。
(2)管道未满,write将数据写入,并返回实际写入的字节数。
(3)可以允许多个写端,但存在竞争关系,读会阻塞第一个写的进程,一旦读到内容,就读取结束,如果多个写在读之前已经全部写完,则全部拿出。
例程:写端打开,3s内不写数据,读端阻塞等待数据,3s后接收到数据停止。
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int ret;
int fd[2];
pid_t pid;
char *str = "hello pipe\n";
char buf[1024] = "\0";
ret = pipe(fd);
if(ret<0)
{
sys_err("pipe error");
}
pid = fork();
if(pid<0)
{
sys_err("fork error");
}
else if(pid>0)
{
close(fd[0]);
sleep(3);
write(fd[1],str,strlen(str));
close(fd[1]);
}
esle
{
close(fd[1]);
ret = read(fd[0],buf,sizeof(buf));
write(STDOUT_FILENO,buf,ret);
close(fd[0]);
}
return 0;
}
标准管道流popen()函数
与Linux的文件操作中基于文件流的标准I/O操作一样,管道操作也支持基于文件流的模式。这种基于文件流的管道主要是用来创建一个连接到另一个进程的管道,这里的“另一个进程”也就是一个可以进行一定操作的可执行文件,例如,用户执行“ls -l”或者自己编写的程序“./pipe”等。
由于这类操作很常用,因此标准流管道就将一系列的创建过程合并到一个函数popen()中完成,它所完成的工作有以下几步:
1.创建一个管道
2.fork()创建一个进程
3.在父子进程中关闭不需要的文件描述符
4.执行exec函数族的调用
5.执行函数中所指定的命令
这个函数的使用可以大大减少代码的编写量,但同时也有一些不利之处。例如,它不如前面管道创建的函数那样灵活多变,并且用popen()创建的管道必须使用标准I/O函数进行操作,而不能使用系统调用函数read()、write()一类不带缓冲的I/O函数。与之相对应,关闭popen()创建的流管道必须使用函数pclose(),该函数关闭标注I/O流,并等待命令执行结束。
popen:创建一个管道
pclose:关闭由popen()打开的管道
表头文件 | #include<stdio.h> | ||
定义函数 | FILE * popen( const char * command,const char * type); | ||
函数说明 | popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。参数type可使用“r”代表读取,“w”代表写入。依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。此外,所有使用文件指针(FILE*)操作的函数也都可以使用,除了fclose()以外。
| ||
返回值 | 若成功则返回文件指针,否则返回NULL,错误原因存于errno中。 | ||
错误代码 | EINVAL参数type不合法。 | ||
注意事项 | 在编写具SUID/SGID权限的程序时请尽量避免使用popen(),popen()会继承环境变量,通过环境变量可能会造成系统安全的问题。 | ||
范例 |
| ||
执行 | root :x:0 0: root: /root: /bin/bash |
例程:调用shell命令cat来打印buf里的内容
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#define BUFF 100
int main()
{
FILE *write_fp;
char buff[BUFF+1];
//把格式化的数据写入某个字符长缓冲区。
//返回值:字符长长度(strlen)
sprintf(buff,"once upon a time,there was...\n");
//格式化输出文件中的数据
//-c:等价于-t c,选择ASCII玛字符或者是转义字符
write_fp = popen("cat > 1.txt","w");
if(write_fp!=NULL)
{
fwrite(buff,sizeof(char),strlen(buff),write_fp);
pclose(write_fp);
}
return 0;
}
FIFO有名管道
命令:mkfifo 管道名
int mkfifo(const char *pathname,mode_t mode);
//成功:0;失败:-1
有名管道创建可以使用函数mkfifo,该函数类似于文件中的open()操作,可以指定管道的路径和打开的模式。
例程:利用mkfifo函数创建FIFO管道
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#define BUFSIZE 1024
void err_quit(char *str)
{
perror(str);
exit(1);
}
int main()
{
int ret = mkfifo("mytextfifo",0664);
if(ret<0)
{
err_quit("mkfifo");
}
return 0;
}
FIFO通信使用
在管道创建成功后,就可以使用open()、write()、read()这些函数了。对于为读而打开的管道可在open()中设置O_RDONLY,同理为写而打开的管道在open中设置O_WRONLY。
默认方式:
1.如果FIFO读没有打开,无法写入内容,write可能处于阻塞状态,当读打开之后,就会立即写入内容,或者成功wtite一次后,自动退出。
2.如果FIFO写没有打开,无法读入内容,read处于阻塞状态,当写入内容后,就会立即读到。
3.当unlink()取消有名管道后,write会自动停止,read可能也会停止。
例程:wrfifo.c负责写入内容,rdfifo负责读出内容
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
#define BUFSIZE 1024
void err_quit(char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int fd,i;
char buf[2048];
if(argc<2)
{
perror("enter");
return -1;
}
fd = open(argv[1],O_WRONLY);
if(fd<0)
{
err_quit("open");
}
i = 0;
for(;i<5;i++)
{
sprintf(buf,"hello bo %d\n",i+1);
int ret = write(fd,buf,strlen(buf));
printf("write:%s ret=%d\n",buf,ret);
sleep(1);
}
close(fd);
unlink("testfifo");
return 0;
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
#define BUFSIZE 1024
void err_quit(char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int fd,len;
char buf[2048];
if(argc<2)
{
perror("enter");
return -1;
}
fd = open(argv[1],O_RDONLY);
if(fd<0)
{
err_quit("open");
}
i = 0;
for(;i<10;i++)
{
len = read(fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,len);
sleep(1);
}
close(fd);
unlink("testfifo");
return 0;
}
对于读可以设置阻塞和非阻塞
在这里与普通文件不同的是阻塞问题。由于普通文件在读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能,这里的非阻塞标志可以在open()函数中设定为O_NONBLOCK。下面分别对阻塞打开和非阻塞打开的读写进行讨论。
对于读进程:
(1)若该管道是阻塞打开,且当前FIFO内没有数据,则对读进程而言将一直阻塞到有据写入。
(2)若该管道是非阻塞打开而不能写入全部数据,则读操作进行部分写入或者调用失败。练习:将读设置为非阻塞方式,连续读5次,发现我没有数据,就反馈超时。
什么是FIFO?
FIFO: First in, First out
代表先进的数据先出 ,后进的数据后出。
为什么需要FIFO?
FIFO存储器是系统的缓冲环节,如果没有FIFO存储器,整个系统就不可能正常工作。
FIFO的功能可以概括为
(1)对连续的数据流进行缓存,防止在进机和存储操作时丢失数据;
(2)数据集中起来进行进机和存储,可避免频繁的总线操作,减轻CPU的负担;
(3)允许系统进行DMA操作,提高数据的传输速度。这是至关重要的一点,如果不采用DMA操作,数据传输将达不到传输要求,而且大大增加CPU的负担,无法同时完成数据的存储工作。
如何通俗的理解FIFO?
其实FIFO理解起来很简单,就像一个水池,如果写通道打开了,就代表我们在加水,如果读通道打开了就代表我们在放水,假如不间断的加水和放水,如果加水速度比放水速度快,那FIFO 就会有满的时候,如果满了还继续加水就会溢出overflow 如果放水速度比加水速度快 ,那么 FIFO就会有空的时候。
access函数
头文件:#include<unistd.h>
函数原型:int access(const char* pathname, int mode);
参数介绍:
pathname 是文件的路径名+文件名
mode:指定access的作用,取值如下:
F_OK 值为0,判断文件是否存在
X_OK 值为1,判断对文件是可执行权限
W_OK 值为2,判断对文件是否有写权限
R_OK 值为4,判断对文件是否有读权限
注:后三种可以使用或“|”的方式,一起使用,如W_OK|R_OK
返回值:成功0,失败-1
#include<stdio.h>
#include<unistd.h>
#include <string.h>
#define fileNAME1 "test"
#define fileNAME2 "./liang"
int main(void)
{
char name[BUFSIZ];//文件名字
int flag = 1;//退出标志,0 exit
printf("\t\t\t程序开始\n");
printf("请输入要检查的文件(可包含路径,EOF退出):");
scanf("%s",name);
if( strcmp(name,"EOF") == 0 ){
flag = 0;
}
while(flag){
if(access(name,F_OK)==0){
printf("文件存在\n");
if(access(name,R_OK|W_OK)==0){
printf("文件可读可写\n");
}else
printf("文件不可读或不可写\n");
if(access(name,X_OK)==0){
printf("文件可执行\n");
}else
printf("文件不可执行\n");
}else
printf("文件不存\n");
printf("\n请输入要检查的文件(可包含路径,EOF退出):");
scanf("%s",name);
if( strcmp(name,"EOF") == 0 )
{
flag = 0;
}
}//while
printf("\t\t\t程序结束\n");
return 0;
}
好了,就先学习到这里,我们下周继续!