这里为什么编译不通过?
因为字符串具有常量属性,字符常量不可被修改。这里的问题是字符串为什么会有常量属性呢?
这里是字符串常量,具有常性,所以存储在了代码段当中的常量区。
因为这里的字符串地址一定是虚拟地址,而改成字符H,是在物理空间上做修改,所以此时就需要页表进行映射,而这里就会有权限的限制,只读的权限,不可修改,所以才会有字符常量不可修改这样的限制,本质是操作系统的锅!
代码段里面存储的是可执行代码和常量区;数据段存储的是全局变量和静态变量
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h> pid_t fork(void); 返回值:自进程中返回0,父进程返回子进程id,出错返回-1
子进程返回0, 父进程返回的是子进程的pid
为什么要用拷贝的形式,父进程直接将资源给子进程不就行了吗?
我们通常的操作有增删改查,可能会直接修改了原来的内容,所以需要额外拷贝一份资源。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式拷贝副本。
在进行拷贝的时候,会将数据段的页表权限改成只读权限!然后任何一方想要进行写入的时候,这个时候操作系统就会介入,将权限改回来可读可写,所以当我们的子进程进行写入的时候就会报错缺页中断。操作系统就会介入,这样就写时拷贝就可以按需进行!
页表不仅仅有将虚拟地址转换为物理内存,还会有权限位
正常终止(可以通过 echo $? 查看进程退出码): 1. 从main返回 2. 调用exit 3. _exit 异常退出: ctrl + c,信号终止
exit函数会支持刷新缓冲区,_exit函数不支持。exit()底层封装了_exit(),两者是上下层关系
退出码包含错误码,当退出码是0的时候,表示程序正常退出;如果退出码!=0,这个退出码就表示错误码。然后利用sterror函数将其进行转换。 进程如果在执行的时候异常了,os会发送信号终止它,这个就是退出信号。非0就代表程序出异常,0代表程序正常执行。 任何进程最终的执行情况,我们可以使用两个数字表明具体的执行情况,一个是退出码,另一个就是退出信号
使用语言或者系统自带的方法进行转化,例如:在linux中,使用strerror()函数。
char* strerror(int errnum);
#include<stdio.h>
#include<string.h>
int main()
{
for(int i = 0; i < 100; i++)
printf("%d:%s\n", i, strerror(i));
return 0;
}
fopen函数举例:返回了非空的FILE*指针,则可认为函数执行成功;返回了NULL,则可认为函数执行失败,需要进一步检查错误的原因(errno变量或调用perror()函数)。
pid_t wait(int* status);
等待任意一个子进程结束,并回收其资源。 返回值:调用成功,返回已经结束进程的PID,同时获取到了子进程的退出状态码;调用失败,返回-1,并设置错误码以指示错误的原因。
参数status:输出型参数,用于存储子进程的退出状态,由OS填充,如果不需要这个信息,可以传递NULL,否则,OS会根据该参数,将子进程的信息反馈给父进程。
pid_t waitpid(pid_t pid, int* status, int options);
等待任意一个子进程或者指定的子进程结束,并回收其资源。 参数pid:如果pid = -1,等待任意一个子进程,与wait等效;如果pid > 0,等待其进程的PID与pid相等的子进程。
参数option:如果option = 0,则为阻塞等待;如果option = WNOHANG,则为非阻塞等待。
调用成功,返回收集到的子进程的PID,同时获取到了子进程的退出状态码;调用失败,返回-1,并设置错误码以指示错误的原因;如果为非阻塞等待,waitpid调用成功且没有收集到已结束的子进程,则返回0。
定义:进程在发出某个请求(如:I/O操作、等待某个条件成立等)后,如果请求不能立即得到满足(如:数据未准备好、资源被占用等),进程会被挂起,在此期间无法继续执行其他任务,直到等待条件满足或被唤醒。 一心一意,专心做一件事!
特点: a.行为 -> 进程在等待期间无法执行其他任务。
b.触发方式 -> 等待由外部条件触发(如:数据到达、资源释放等)。
c.管理层面:由操作系统或者底层系统资源管理。
d.效率与并发性:效率低。
应用场景:实时性要求不高,等待时间相对比较短的情况,如:简单文件的读写操作。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0) //子进程
{
int cnt = 5;
while(cnt)
{
printf("child is running, id:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1); //子进程退出
}
int status = 0; //存储子进程退出状态
pid_t rid = waitpid(id, &status, 0); //父进程等待 —— 阻塞等待
if(rid > 0) //等待成功
printf("wait success, status:%d\n", status);
else if(rid == -1) //调用失败
perror("wait error!\n");
return 0;
}
定义:进程在发出某个请求后,不会被立即挂起已等待请求的完成,即使请求不能立即得到满足,进程在等待期间可以继续执行其他任务,同时可能会以某种方式(轮询访问、回调等)定期检查请求状态或者等待结果的通知。
特点: a.行为 -> 进程在等待期间可以执行其他任务;
b.触发方式 -> 可能通过编程的方式实现,如:轮询、回调等。
c.管理层面:在应用层通过编程实现。
d.效率与并发性:效率高,提高并发性和响应能力。
应用场景:需要高并发和响应能力的场景,如:在网络编程中,服务器同时处理多个客户端的请求。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define SIZE 5
int main()
{
Inittask();
pid_t id = fork();
if(id == 0) //子进程
{
int cnt = 2;
while(cnt)
{
printf("I am a process, id:%d, ppid:%d\n", getpid(), getppid()); sleep(1);
cnt--;
}
exit(1); //子进程退出
}
int status = 0; //存储子进程退出状态
while(1) //基于非阻塞轮询的访问
{
pid_t rid = waitpid(id, &status, WNOHANG); //非阻塞等待
if(rid > 0) //调用成功,收集到了已经结束的子进程 {
printf("wait success, status:%d\n", status);
break;
}
else if(rid == 0) //调用成功,未收集到已经结束的子进程
{
printf("child is running, father do other thing!\n");
printf("------------ Task begin ----------------\n");
executeTask(); //等待期间,执行其他任务
printf("------------ Task end ----------------\n");
}
else //调用失败
{
perror("wait error\n");
break;
}
sleep(1);
}
return 0;
}
我们这里只讲解16位的情况下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0) //子进程
{
int cnt = 5;
while(cnt)
{
printf("child is running, id:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
exit(1); //子进程退出
}
int status = 0; //存储子进程退出状态
pid_t rid = waitpid(id, &status, 0);
if(rid > 0) //等待成功
printf("wait success, status:%d, exit code:%d, exit sign:%d\n",
status, (status>>8)&0xff, status&0x7f); //位操作获取子进程的退出码、退出信号
return 0;
}
它允许一个进程在执行期间,用一个新的程序来替换当前正常执行的程序,即:用全新的程序替换原有的程序。 这意味着进程在调用一种exec函数,当前进程的用户空间代码和数据被新程序的代码和数据完全替换(覆盖),从新程序的启动例程开始执行。
注意:调用exec函数,并不会创建新的进程,而是对原有进程的资源进行替换,因此调用exec前后该进程的pid并未发生改变。
加载新程序 -> 替换当前程序 -> 更新页表 -> 执行新程序。
有六种以exec开头的函数,统称exec函数:
#include <unistd.h>` int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ...,char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]);
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。 如果调用出错则返回-1 所以exec函数只有出错的返回值而没有成功的返回值。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。 l(list) : 表示参数采用列表 v(vector) : 参数用数组 p(path) : 有p自动搜索环境变量PATH e(env) : 表示自己维护环境变量
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示
我们要知道当我们把一个程序进行./,那么这个程序就变成了一个进程,而在我们的这个进程中执行了关于进程替换的函数,那么该进程就会被替换,执行另一个进程!
我们不一定要让一个进程直接进行替换,可以创建子进程,让子进程进行替换,让父进程等待我们的结果就可以.
因为进程具有独立性,我们将子进程进行替换,发生写时拷贝,不会影响父进程
一次想生成两个可执行文件,就需要这么写,不然makefile默认值生成第一条指令!